第一版

This commit is contained in:
Ls
2026-04-16 08:38:54 +08:00
parent c9aeba0949
commit 7dba9711a9
29 changed files with 5832 additions and 2447 deletions

303
AGENTS.md
View File

@@ -1,143 +1,212 @@
# AGENTS.md - Swimming uni-app Project # AGENTS.md - Swimming uni-app Project
## Build & Development Commands ## Scope
- This file applies to the entire repository rooted at `D:\Project\游泳\swimming`.
- There are currently no deeper `AGENTS.md` files in subdirectories.
- No `.cursor/rules/`, `.cursorrules`, or `.github/copilot-instructions.md` files exist in this repo.
### Development Servers ## Project Overview
- This is a `uni-app` + Vue 3 + TypeScript project built with Vite.
- Main app source lives under `src/`.
- The app appears to target H5, app, and multiple mini-program platforms.
- Primary UI library is `uview-plus`.
- Charting-related code exists under `src/uni_modules/qiun-data-charts` and `src/uni_modules/lime-echart`.
- Service-layer code is centralized under `src/Service/`.
## Tooling Snapshot
- Package manager: `npm`.
- Bundler/dev server: `vite` via `@dcloudio/vite-plugin-uni`.
- Type checking: `vue-tsc --noEmit`.
- Language level: TypeScript `^4.9.4`.
- Framework: Vue `^3.4.21`.
- No ESLint config is present.
- No Prettier config is present.
- No unit or e2e test runner config is present.
## Install
```bash ```bash
npm run dev:h5 # Run H5 development server npm install
npm run dev:mp-weixin # Run WeChat mini-program development
npm run dev:app # Run app development
npm run dev:app-android # Run Android app development
npm run dev:app-ios # Run iOS app development
npm run dev:mp-alipay # Run Alipay mini-program
npm run dev:mp-baidu # Run Baidu mini-program
npm run dev:mp-qq # Run QQ mini-program
npm run dev:mp-toutiao # Run Toutiao mini-program
``` ```
### Build Commands ## Build And Dev Commands
### Common Development
```bash ```bash
npm run build:h5 # Build for H5 npm run dev:h5
npm run build:mp-weixin # Build for WeChat mini-program npm run dev:h5:ssr
npm run build:app # Build for app npm run dev:app
npm run build:app-android # Build for Android npm run dev:app-android
npm run build:app-ios # Build for iOS npm run dev:app-ios
npm run dev:mp-weixin
npm run dev:mp-alipay
npm run dev:mp-baidu
npm run dev:mp-jd
npm run dev:mp-kuaishou
npm run dev:mp-lark
npm run dev:mp-qq
npm run dev:mp-toutiao
npm run dev:mp-xhs
npm run dev:quickapp-webview
npm run dev:quickapp-webview-huawei
npm run dev:quickapp-webview-union
``` ```
### Type Checking ### Common Build
```bash ```bash
npm run type-check # Run TypeScript type checking (vue-tsc --noEmit) npm run build:h5
npm run build:h5:ssr
npm run build:app
npm run build:app-android
npm run build:app-ios
npm run build:mp-weixin
npm run build:mp-alipay
npm run build:mp-baidu
npm run build:mp-jd
npm run build:mp-kuaishou
npm run build:mp-lark
npm run build:mp-qq
npm run build:mp-toutiao
npm run build:mp-xhs
npm run build:quickapp-webview
npm run build:quickapp-webview-huawei
npm run build:quickapp-webview-union
``` ```
### Testing ### Validation
- **Note**: No test framework configured in this project yet. ```bash
npm run type-check
## Code Style Guidelines
### Imports & Path Aliases
- Use `@/` alias for imports from `src/` directory (configured in `tsconfig.json`)
- Example: `import { Service } from '@/Service/Service'`
- Import Vue composition API functions from 'vue'
- Import uni-app lifecycle hooks from `@dcloudio/uni-app`
### File Structure
``` ```
## Lint And Test Status
- There is no configured lint command in `package.json`.
- There is no ESLint or Prettier configuration in the repository root.
- There is no configured test framework such as Vitest, Jest, Playwright, Cypress, Mocha, or Ava.
- There are no repository test files matching `*.spec.*` or `*.test.*` in app code.
- Because no test runner exists, there is currently no supported command for running a single test.
- For validation, prefer `npm run type-check` and, when relevant, a targeted platform build such as `npm run build:h5`.
## Single-Test Guidance
- Single-test execution is not available in the current repo state.
- If a future test runner is added, update this file with:
- the root test command,
- the single-test command pattern,
- any platform-specific test setup,
- and the location/naming convention for test files.
## Important Paths
- `src/main.ts` bootstraps the app and registers `uview-plus`.
- `src/App.vue` contains global app shell styles and setup.
- `src/pages.json` defines pages and tab bar configuration.
- `src/uni.scss` contains shared uni-app styling variables and theme-level styles.
- `src/Service/` contains application services and shared app behavior.
- `src/common/` contains shared domain models and utilities.
- `src/components/` contains reusable Vue components.
- `src/pages/` contains page-level Vue SFCs.
- `src/uni_modules/` contains vendored or external uni-app modules.
## Source Layout Conventions
```text
src/ src/
├── pages/ # Page components (with sub-packages) ├── Service/ Service classes and base config
├── components/ # Reusable Vue components ├── common/ Shared utilities and domain models
├── Service/ # API services and utilities ├── components/ Reusable Vue SFC components
├── common/ # Common utilities and helpers ├── pages/ Routed uni-app pages
├── static/ # Static assets (images, etc.) ├── static/ Static assets
├── types/ # TypeScript type definitions ├── types/ Type declarations
├── uni_modules/ # Uni-app modules ├── uni_modules/ Third-party or packaged uni modules
── colorui/ # ColorUI CSS framework ── colorui/ ColorUI-related assets/components
├── App.vue Root application component
├── main.ts App bootstrap
├── pages.json Route and tab-bar registration
└── uni.scss Global uni-app SCSS
``` ```
### Naming Conventions ## Imports And Module Usage
- **Files**: PascalCase for components (e.g., `ImageCropper.vue`), camelCase for utilities - Use the `@/` alias for imports rooted in `src/`.
- **Components**: PascalCase for component names - `tsconfig.json` maps `@/*` to `./src/*`.
- **Variables**: camelCase for local variables and functions - Prefer alias imports for app code instead of long relative paths.
- **Constants**: UPPER_SNAKE_CASE for constants - Import Vue composition helpers from `vue`.
- **Classes**: PascalCase for class names (e.g., `Service`) - Import uni-app page lifecycle APIs from `@dcloudio/uni-app` when needed.
- **CSS Classes**: kebab-case (e.g., `home-container`, `timer-card`) - Keep imports grouped with framework imports first, then app services/types, then local modules.
- Existing code sometimes re-exports shared utilities from `src/common/Common.ts`; preserve that pattern where it already exists.
### TypeScript ## Vue SFC Conventions
- Use `lang="ts"` in Vue SFC `<script>` tags - Use Vue 3 `<script setup lang="ts">` for page and component SFCs.
- Use `<script setup>` syntax for Vue 3 composition API - Keep file sections in the order: `<template>`, `<script setup lang="ts">`, `<style lang="scss" scoped>`.
- Type function parameters and return values - Use `scoped` styles for page/component-local styling unless a global rule is required.
- Use `any` sparingly, prefer proper type definitions - Prefer reactive state via `ref` and `reactive` from Vue.
- Path alias: `@/*` maps to `./src/*` - Use uni-app page lifecycle hooks such as `onLoad` from `@dcloudio/uni-app`.
- Clean up timers or intervals in `onUnmounted()`.
### Vue Component Structure ## TypeScript Guidelines
```vue - Use `lang="ts"` in Vue SFC scripts.
<template> - Add explicit parameter types and return types for exported functions and service methods where practical.
<!-- Template with tabs for indentation --> - Prefer concrete interfaces or domain models over `any`.
</template> - Existing code uses `any` in several places; reduce new `any` usage instead of expanding it.
- Keep class names in PascalCase.
- Preserve existing static-service patterns unless there is a strong reason to refactor more broadly.
<script setup lang="ts"> ## Service Layer Patterns
// Imports first - Base configuration lives in `src/Service/BaseConfig.ts`.
// Composition API logic - Shared service helpers live in `src/Service/Service.ts`.
// Functions - Feature services under `src/Service/swimming/` usually:
// Lifecycle hooks - define endpoint path constants as `private static` fields,
</script> - expose `static` methods,
- delegate network calls to `Service.Request()`,
- and export both the service class and `Service` where existing files already do so.
- `Service.Request()` wraps request handling and normalizes API responses into `ResultData`.
- `ResultData` lives in `src/common/Domain/ResultData.ts`.
<style lang="scss" scoped> ## Error Handling And User Feedback
/* SCSS styles with nested structure */ - Use `Service.Msg()` for toast-style user feedback.
</style> - Use `Service.Alert()` or `Service.Confirm()` for modal interactions.
``` - Route authenticated API requests through `Service.Request()`.
- Be aware that `Service.Request()` already handles several auth and permission codes such as `401`, `40101`, `1004`, and `40188`.
- When adding new service methods, return the request promise rather than swallowing errors silently.
- Follow existing user-facing Chinese messaging style unless the surrounding screen uses a different language.
### Formatting ## Naming Conventions
- **Indentation**: Use tabs (not spaces) for indentation - Vue component filenames: PascalCase, for example `ImageCropper.vue`.
- **Quotes**: Single quotes for strings in TypeScript/JavaScript - Utility and service helper filenames: usually camelCase or existing established names; match nearby files.
- **Semicolons**: Optional but consistent (project uses semicolons) - Service classes: PascalCase for class names, for example `PlanService`.
- **Line endings**: CRLF (Windows) - Some existing service class identifiers start lowercase, such as `studentService`; preserve existing public names unless a broader refactor is requested.
- **SCSS**: Use nested selectors, kebab-case class names - Local variables and functions: camelCase.
- Constants: UPPER_SNAKE_CASE when they are true constants.
- CSS class names: kebab-case.
- Route paths must match entries in `src/pages.json`.
### Error Handling ## Formatting Expectations
- Use `Service.Msg()` for toast messages - Repository convention is tabs for indentation in source files.
- Use `Service.Alert()` for modal dialogs - Existing project code mostly uses single quotes in application TypeScript, though some files contain double quotes; prefer the dominant local style in the file you touch.
- API requests through `Service.Request()` handle 401 (token expired) automatically - Semicolons are used frequently enough that new changes should stay consistent within the edited file.
- Return `ResultData` objects from service methods - Keep line endings as CRLF on Windows-oriented files when possible.
- Clean up intervals in `onUnmounted()` lifecycle hook - Use nested SCSS selectors where that is already the local pattern.
- Avoid introducing broad formatting-only diffs.
### UI Components ## Styling And UI
- Use **uview-plus** as primary UI component library - Prefer `uview-plus` components where the project already uses them.
- Use **ColorUI** for CSS framework and icons - ColorUI assets/components are also present; preserve established usage where relevant.
- Component prefix: `u-` for uview-plus components (e.g., `<u-icon>`, `<u-button>`) - Use `rpx` units for responsive uni-app layouts.
- Keep mobile-first layout assumptions.
- Match the visual language already present on the page you are editing rather than redesigning unrelated UI.
### Service Layer Pattern ## Platform And Runtime Notes
- Extend `BaseConfig` for service classes - Use `uni.*` APIs for platform-specific behavior.
- Use static methods for utility functions - Navigation typically goes through `Service.GoPage()`, `Service.GoPageTab()`, `Service.GoPageBack()`, or `Service.GoPageDelse()`.
- `Service` class provides: - App state and auth token access are commonly wrapped by `Service.GetStorageCache()` and related helpers.
- API requests: `Service.Request()` - Be careful when changing files inside `src/uni_modules/`; many are third-party modules and should only be edited when necessary.
- Navigation: `Service.GoPage()`, `Service.GoPageTab()`, `Service.GoPageBack()`
- Storage: `Service.SetStorageCache()`, `Service.GetStorageCache()`
- Messages: `Service.Msg()`, `Service.Alert()`
- Loading: `Service.LoadIng()`, `Service.LoadClose()`
### uni-app Specifics ## Agent Guidance
- Use `uni.` API for platform-specific operations - Before changing code, inspect nearby files and follow their existing conventions.
- Pages registered in `src/pages.json` - Keep edits focused and minimal; do not refactor unrelated areas opportunistically.
- Tab bar configured in `src/pages.json` - Do not invent lint or test commands that are not actually configured.
- Use `rpx` units for responsive design - When describing validation, use real commands from `package.json`.
- Global styles in `src/App.vue` and `src/uni.scss` - If you add a test framework or lint setup later, update this file immediately.
- If you touch vendored `uni_modules`, call that out explicitly in your final summary.
### Git & Commit ## Current Gaps To Remember
- `.gitignore` includes `node_modules/`, `dist/`, `unpackage/` - No lint pipeline is configured.
- Commit messages should be descriptive (in Chinese or English) - No automated test pipeline is configured.
- No single-test command exists yet.
### Key Dependencies - No Cursor or Copilot repository instruction files exist.
- Vue 3.4.21 - Agents should rely on local code patterns plus this file when making changes.
- TypeScript 4.9.4
- Vite 5.2.8
- uni-app 3.0.0
- uview-plus 3.3.54
- dayjs (date manipulation)
- echarts (charts)
- vue-i18n (internationalization)
### No Existing Rules
- No `.cursor/rules/` or `.cursorrules` found
- No `.github/copilot-instructions.md` found
- No ESLint or Prettier configuration found
- Follow existing code patterns when making changes

8
package-lock.json generated
View File

@@ -26,7 +26,7 @@
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"uview-plus": "^3.3.54", "uview-plus": "^3.7.32",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-i18n": "^9.1.9" "vue-i18n": "^9.1.9"
}, },
@@ -12682,9 +12682,9 @@
} }
}, },
"node_modules/uview-plus": { "node_modules/uview-plus": {
"version": "3.7.13", "version": "3.7.32",
"resolved": "https://registry.npmmirror.com/uview-plus/-/uview-plus-3.7.13.tgz", "resolved": "https://registry.npmmirror.com/uview-plus/-/uview-plus-3.7.32.tgz",
"integrity": "sha512-vHByf0kxKReYxam6BuU6wn/80giCkMaMUHEblhkf4kAjP852b86V3ctkjfGtV17MEIORFo3Vkve+HFnHNXpwNg==", "integrity": "sha512-sSUHHP0xgGkMoipRFB9XpiqTR/eaBn4WwF+rFQRcPLbr3CCey0hwH700HoqX6Uo6IsKensFIhv6Tw6Rs8XWv5w==",
"engines": { "engines": {
"HBuilderX": "^3.1.0", "HBuilderX": "^3.1.0",
"uni-app": "^4.66", "uni-app": "^4.66",

View File

@@ -41,6 +41,83 @@ class PlanService {
} }
private static UpdateGroupStudentPath : string = '/api/Plan/UpdateGroupStudent';
/*****修改分组学生学生接口*****/
static UpdateGroupStudent(data:any) {
var result = Service.Request(this.UpdateGroupStudentPath, "POST", data);
return result;
}
private static AddStudentToGroupPath : string = '/api/Plan/AddStudentToGroup';
/*****添加分组学生接口*****/
static AddStudentToGroup(data:any) {
var result = Service.Request(this.AddStudentToGroupPath, "POST", data);
return result;
}
private static RemoveStudentFromGroupPath : string = '/api/Plan/RemoveStudentFromGroup';
/*****从分组中删除学生接口*****/
static RemoveStudentFromGroup(data:any) {
var result = Service.Request(this.RemoveStudentFromGroupPath, "POST", data);
return result;
}
private static AddPlanLogPath : string = '/api/Plan/AddPlanLog';
/*****添加记录接口*****/
static AddPlanLog(planId:string,planType:string,timingEvents:string,subsection:string,baogan:string) {
var result = Service.Request(this.AddPlanLogPath, "POST", {planId,planType,timingEvents,subsection,baogan });
return result;
}
private static GetPlanListNoPagePath : string = '/api/Plan/GetPlanListNoPage';
/*****不分页项目接口*****/
static GetPlanListNoPage(planType:string) {
var result = Service.Request(this.GetPlanListNoPagePath, "GET", {planType});
return result;
}
private static GetBaoganLogPath : string = '/api/Plan/GetBaoganLog';
/*****包干数据接口*****/
static GetBaoganLog(data:any) {
var result = Service.Request(this.GetBaoganLogPath, "GET", data);
return result;
}
private static GetJishiLogPath : string = '/api/Plan/GetJishiLog';
/*****计时数据接口*****/
static GetJishiLog(data:any) {
var result = Service.Request(this.GetJishiLogPath, "GET", data);
return result;
}
private static GetQuxianLogPath : string = '/api/Plan/GetQuxianLog';
/*****曲线数据接口*****/
static GetQuxianLog(data:any) {
var result = Service.Request(this.GetQuxianLogPath, "GET", data);
return result;
}
private static GetFenduanLogPath : string = '/api/Plan/GetFenduanLog';
/*****分段接口*****/
static GetFenduanLog(data:any) {
var result = Service.Request(this.GetFenduanLogPath, "GET", data);
return result;
}
private static GetRankDataPath : string = '/api/Plan/GetRankData';
/*****成绩排名接口*****/
static GetRankData(planId:string) {
var result = Service.Request(this.GetRankDataPath, "GET", {planId });
return result;
}
} }
export { export {

View File

@@ -8,33 +8,49 @@
</view> </view>
</view> </view>
<!-- ==================== 项目选择区域 ==================== --> <!-- 项目选择区域 -->
<view class="project-select-section"> <view class="select-section">
<view class="section-header"> <view class="select-label">
<text class="section-title">选择项目</text> <view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
</view> <view class="picker-wrapper" @click="showProject=true">
<!-- 项目选择器 --> <text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex"
@change="handleProjectChange">
<view class="picker-wrapper">
<text class="picker-text">{{ selectedProjectName }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon> <u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view> </view>
</picker> </view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
:columns="projectOptions"></up-picker>
<!-- 日期选择 -->
<view class="date-filter" style="margin: 20rpx 20rpx 0;" v-if="selectProcet">
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">开始日期</text>
<text class="value">{{ begin || '请选择' }}</text>
</view>
</view>
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">结束日期</text>
<text class="value">{{ end || '请选择' }}</text>
</view>
</view>
</view> </view>
<!-- ==================== 学生选择区域 ==================== --> <!-- ==================== 学生选择区域 ==================== -->
<!-- 仅在选择项目后显示 --> <!-- 仅在选择项目和时间后显示 -->
<view class="student-select-section" v-if="selectedProjectId && projectStudents.length > 0"> <view class="student-select-section" v-if="selectProcet && begin && projectStudents.length > 0">
<view class="section-header"> <view class="section-header">
<text class="section-title">选择学员最多3人</text> <text class="section-title">选择学员最多3人</text>
<text class="select-count">已选{{ selectedStudentIds.length }}/3</text> <text class="select-count">已选{{ selectedStudentIds.length }}/3</text>
</view> </view>
<!-- 学生列表 --> <!-- 学生列表 -->
<view class="student-list"> <view class="student-list">
<view v-for="student in projectStudents" :key="student.id" <view v-for="student in projectStudents" :key="student.studentId"
:class="['student-item', { 'selected': selectedStudentIds.includes(student.id) }]" :class="['student-item', { 'selected': selectedStudentIds.includes(student.studentId) }]"
@click="toggleStudent(student.id)"> @click="toggleStudent(student.studentId)">
<!-- 学生头像使用首字母作为头像 --> <!-- 学生头像使用首字母作为头像 -->
<view class="student-avatar"> <view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text> <text class="avatar-text">{{ student.name.charAt(0) }}</text>
@@ -42,7 +58,7 @@
<!-- 学生姓名 --> <!-- 学生姓名 -->
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
<!-- 选择状态图标 --> <!-- 选择状态图标 -->
<view class="check-icon" v-if="selectedStudentIds.includes(student.id)"> <view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)">
<u-icon name="checkmark" size="16" color="#fff"></u-icon> <u-icon name="checkmark" size="16" color="#fff"></u-icon>
</view> </view>
</view> </view>
@@ -57,18 +73,14 @@
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">选中项目</text> <text class="stat-label">选中项目</text>
<text class="stat-value">{{ selectedProjectName }}</text> <text class="stat-value">{{ selectProcet }}</text>
</view> </view>
<view class="stat-divider"></view> <view class="stat-divider"></view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">对比学员</text> <text class="stat-label">对比学员</text>
<text class="stat-value">{{ selectedStudentIds.length }}</text> <text class="stat-value">{{ selectedStudentIds.length }}</text>
</view> </view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">训练次数</text>
<text class="stat-value">{{ maxTrainingCount }}</text>
</view>
</view> </view>
<!-- 折线图卡片 --> <!-- 折线图卡片 -->
@@ -85,18 +97,12 @@
</scroll-view> </scroll-view>
</view> </view>
<!-- 图例说明 -->
<view class="legend-section">
<text class="legend-title">图例说明</text>
<view class="legend-list">
<view v-for="(student, index) in selectedStudents" :key="student.id" class="legend-item">
<view class="legend-color" :style="{ backgroundColor: chartColors[index] }"></view>
<text class="legend-name">{{ student.name }}</text>
<text class="legend-desc">{{ getStudentBestTime(student.id) }}s 最佳</text>
</view>
</view>
</view>
</view> </view>
<!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm"
@close="calendarClose">
</up-calendar>
</view> </view>
</template> </template>
@@ -104,6 +110,7 @@
import { onShow, onLoad } from "@dcloudio/uni-app" import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
// ==================== 常量定义 ==================== // ==================== 常量定义 ====================
@@ -115,30 +122,17 @@
// ==================== 响应式数据 - 项目相关 ==================== // ==================== 响应式数据 - 项目相关 ====================
// 项目列表数据 // 项目选择
const projectList = ref([ let showProject = ref(false)
{ id: '1', name: '100米自由泳' }, let selectProcet = ref('')
{ id: '2', name: '50米自由泳' }, let selectId = ref('')
{ id: '3', name: '200米由泳' },
{ id: '4', name: '100米蛙泳' }
])
// 项目选择器选项(用于 picker 组件) // 日期选择
const projectOptions = ref(projectList.value) const begin = ref<string>('')
const end = ref<string>('')
const showCalendar = ref(false)
// 当前选中的项目索引(用于 picker 组件的显示) const projectOptions = ref<Array<any>>([[]])
const selectedProjectIndex = ref(-1)
// 当前选中的项目 ID
const selectedProjectId = ref('')
// 当前选中的项目名称(用于显示)
const selectedProjectName = computed(() => {
if (selectedProjectIndex.value === -1) {
return '请选择项目'
}
return projectList.value[selectedProjectIndex.value]?.name || '请选择项目'
})
// ==================== TypeScript 接口定义 ==================== // ==================== TypeScript 接口定义 ====================
@@ -147,7 +141,7 @@
* 定义学生的基本信息 * 定义学生的基本信息
*/ */
interface Student { interface Student {
id : string studentId : string
name : string name : string
} }
@@ -175,7 +169,7 @@
// 已选中的学生详细信息列表((计算属性) // 已选中的学生详细信息列表((计算属性)
const selectedStudents = computed(() => { const selectedStudents = computed(() => {
return projectStudents.value.filter(student => return projectStudents.value.filter(student =>
selectedStudentIds.value.includes(student.id) selectedStudentIds.value.includes(student.studentId)
) )
}) })
@@ -227,129 +221,59 @@
series: [] as any[] series: [] as any[]
}) })
// 最大训练次数(计算属性)
const maxTrainingCount = computed(() => {
const allRecords = mockTrainingRecords[selectedProjectId.value] || []
const studentCounts = new Map<string, number>()
allRecords.forEach(record => {
studentCounts.set(record.studentId, (studentCounts.get(record.studentId) || 0) + 1)
})
return Math.max(0, ...Array.from(studentCounts.values()))
})
// ==================== 模拟数据 ====================
// 模拟的学生数据(按项目分组)
const mockProjectStudents : Record<string, Student[]> = {
'1': [
{ id: 's1', name: '张小明' },
{ id: 's2', name: '李小红' },
{ id: 's3', name: '王小明' },
{ id: 's4', name: '赵小芳' },
{ id: 's5', name: '陈小刚' }
],
'2': [
{ id: 's6', name: '刘小华' },
{ id: 's7', name: '张小丽' },
{ id: 's8', name: '杨小龙' }
],
'3': [
{ id: 's9', name: '黄小东' },
{ id: 's10', name: '吴小西' },
{ id: 's11', name: '周小南' },
{ id: 's12', name: '徐小北' }
],
'4': [
{ id: 's13', name: '马小兵' },
{ id: 's14', name: '朱小红' }
]
}
// 模拟的训练记录数据(按项目分组)
const mockTrainingRecords : Record<string, StudentTrainingRecord[]> = {
'1': [
{ studentId: 's1', studentName: '张小明', time: 28.5, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's1', studentName: '张小明', time: 27.2, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 },
{ studentId: 's1', studentName: '张小明', time: 26.5, recordDate: '03-16', recordFullDate: '2026-03-16', round: 1 },
{ studentId: 's1', studentName: '张小明', time: 25.8, recordDate: '03-19', recordFullDate: '2026-03-19', round: 1 },
{ studentId: 's1', studentName: '张小明', time: 25.2, recordDate: '03-24', recordFullDate: '2026-03-24', round: 1 },
{ studentId: 's2', studentName: '李小红', time: 32.5, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's2', studentName: '李小红', time: 31.2, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 },
{ studentId: 's2', studentName: '李小红', time: 30.5, recordDate: '03-19', recordFullDate: '2026-03-19', round: 1 },
{ studentId: 's2', studentName: '李小红', time: 29.8, recordDate: '03-24', recordFullDate: '2026-03-24', round: 1 },
{ studentId: 's3', studentName: '王小明', time: 25.8, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's3', studentName: '王小明', time: 24.5, recordDate: '03-16', recordFullDate: '2026-03-16', round: 1 },
{ studentId: 's3', studentName: '王小明', time: 23.8, recordDate: '03-24', recordFullDate: '2026-03-24', round: 1 },
{ studentId: 's4', studentName: '赵小芳', time: 33.5, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 },
{ studentId: 's4', studentName: '赵小芳', time: 32.2, recordDate: '03-19', recordFullDate: '2026-03-19', round: 1 },
{ studentId: 's5', studentName: '陈小刚', time: 35.5, recordDate: '03-19', recordFullDate: '2026-03-19', round: 1 }
],
'2': [
{ studentId: 's6', studentName: '刘小华', time: 24.5, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's6', studentName: '刘小华', time: 23.8, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 },
{ studentId: 's6', studentName: '刘小华', time: 23.2, recordDate: '03-19', recordFullDate: '2026-03-19', round: 1 },
{ studentId: 's7', studentName: '张小丽', time: 26.8, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's7', studentName: '张小丽', time: 26.2, recordDate: '03-16', recordFullDate: '2026-03-16', round: 1 },
{ studentId: 's8', studentName: '杨小龙', time: 25.5, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 },
{ studentId: 's8', studentName: '杨小龙', time: 24.8, recordDate: '03-19', recordFullDate: '2026-03-19', round: 1 }
],
'3': [
{ studentId: 's9', studentName: '黄小东', time: 125.8, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's9', studentName: '黄小东', time: 124.5, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 },
{ studentId: 's10', studentName: '吴小西', time: 128.5, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's10', studentName: '吴小西', time: 127.2, recordDate: '03-16', recordFullDate: '2026-03-16', round: 1 },
{ studentId: 's10', studentName: '吴小西', time: 126.8, recordDate: '03-19', recordFullDate: '2026-03-19', round: 1 }
],
'4': [
{ studentId: 's13', studentName: '马小兵', time: 82.5, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 },
{ studentId: 's13', studentName: '马小兵', time: 81.8, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 },
{ studentId: 's14', studentName: '朱小红', time: 88.5, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 }
]
}
// ==================== 生命周期钩子 ====================
onLoad(() => { onLoad(() => {
loadProjectData() getProjectData()
}) })
onShow(() => { onShow(() => {
// TODO: 如果需要在页面显示时刷新数据,可以在这里添加逻辑
}) })
// ==================== 监听器 ====================
// 监听选中学生 ID 列表的变化
// 使用 getter 函数返回数组副本,这样可以确保监听器触发
watch(() => [...selectedStudentIds.value], (newIds, oldIds) => {
console.log('selectedStudentIds changed:', newIds) const getProjectData = () => {
console.log('oldIds:', oldIds) PlanService.GetPlanListNoPage('计时项目').then(res => {
updateChartData() if (res.code == 0) {
projectOptions.value[0] = res.data.list
} else {
Service.Msg(res.msg)
}
}) })
// ==================== 业务逻辑方法 ====================
const loadProjectData = () => {
// TODO: 调用后端 API 获取项目列表
} }
const handleProjectChange = (e : any) => { const selectProcetFunc = (e : any) => {
const index = e.detail.value selectProcet.value = e.value[0].name
selectedProjectIndex.value = index selectId.value = e.value[0].planId
const selectedProject = projectList.value[index]
if (selectedProject) {
selectedProjectId.value = selectedProject.id
loadProjectStudents(selectedProject.id)
}
selectedStudentIds.value = [] selectedStudentIds.value = []
loadProjectStudents(selectId.value)
}
const openCalendar = () => {
showCalendar.value = true
}
const calendarConfirm = (e : any) => {
begin.value = e[0]
end.value = e[e.length - 1]
showCalendar.value = false
}
const calendarClose = () => {
showCalendar.value = false
} }
const loadProjectStudents = (projectId : string) => { const loadProjectStudents = (projectId : string) => {
if (mockProjectStudents[projectId]) { PlanService.GetPlanInfo(projectId).then(res => {
projectStudents.value = mockProjectStudents[projectId] if (res.code == 0) {
projectStudents.value = res.data.plan.users
} else { } else {
projectStudents.value = [] Service.Msg(res.msg)
} }
})
} }
const toggleStudent = (studentId : string) => { const toggleStudent = (studentId : string) => {
@@ -363,57 +287,44 @@
} }
selectedStudentIds.value.push(studentId) selectedStudentIds.value.push(studentId)
} }
let studentIdList = selectedStudentIds.value.map((item) => {
return item
})
let data = {
planId: selectId.value,
studentId: JSON.stringify(studentIdList),
sTime: begin.value,
eTime: end.value
}
interface DataItem {
name : string
data : any[]
} }
const getStudentBestTime = (studentId : string) : number => {
const records = mockTrainingRecords[selectedProjectId.value] || [] const xData = ref<DataItem[]>(
const studentRecords = records.filter(r => r.studentId === studentId) Array.from({ length: studentIdList.length }, () => ({
if (studentRecords.length === 0) return 0 name: '',
return Math.min(...studentRecords.map(r => r.time)) data: []
}))
)
PlanService.GetQuxianLog(data).then(res => {
if (res.code == 0) {
res.data.list.map((item : any) => {
lineChartData.value.categories.push(item.dayTime)
item.data.map((content : any, index : any) => {
xData.value[index].name = content.studentName
xData.value[index].data.push(content.time)
})
})
console.log(xData.value);
lineChartData.value.series = xData.value
} else {
Service.Msg(res.msg)
} }
const updateChartData = () => {
if (selectedStudentIds.value.length === 0 || !selectedProjectId.value) {
lineChartData.value.series = []
lineChartData.value.categories = []
return
}
const projectRecords = mockTrainingRecords[selectedProjectId.value] || []
const allDates = new Set<string>()
projectRecords.forEach(record => {
allDates.add(record.recordFullDate)
}) })
const sortedDates = Array.from(allDates).sort()
lineChartData.value.categories = sortedDates.map(date => {
const parts = date.split('-')
return `${parts[1]}-${parts[2]}`
})
const seriesData : any[] = []
selectedStudentIds.value.forEach((studentId, index) => {
const studentRecords = projectRecords.filter(r => r.studentId === studentId)
studentRecords.sort((a, b) => a.recordFullDate.localeCompare(b.recordFullDate))
const timeMap = new Map<string, number>()
studentRecords.forEach(record => {
timeMap.set(record.recordFullDate, record.time)
})
const timeData = sortedDates.map(date => timeMap.get(date) || null)
const studentName = projectStudents.value.find(s => s.id === studentId)?.name || '未知学员'
seriesData.push({
name: studentName,
data: timeData,
color: chartColors[index]
})
})
console.log();
lineChartData.value.series = seriesData
} }
</script> </script>
@@ -466,12 +377,55 @@
} }
} }
.project-select-section { .date-filter {
display: flex;
gap: 24rpx;
.date-picker {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
.date-text {
.label {
display: block;
font-size: 26rpx;
color: #999;
}
.value {
display: block;
font-size: 30rpx;
color: #333;
margin-top: 4rpx;
font-weight: 600;
}
}
}
}
.select-section {
background-color: #fff; background-color: #fff;
margin: 0 20rpx 20rpx; margin: 0 20rpx 20rpx;
border-radius: 16rpx; border-radius: 20rpx;
padding: 28rpx; padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
.select-label {
margin-bottom: 16rpx;
.label-text {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
}
.picker-wrapper { .picker-wrapper {
display: flex; display: flex;
@@ -485,7 +439,8 @@
&:active { &:active {
background-color: #f0f0f0; background-color: #f0f0f0;
border-color: #52c41a; border-color: #faad14;
transform: scale(0.98);
} }
.picker-text { .picker-text {

View File

@@ -11,26 +11,36 @@
<!-- 项目选择区域 --> <!-- 项目选择区域 -->
<view class="select-section"> <view class="select-section">
<view class="select-label"> <view class="select-label">
<text class="label-text">选择项目</text> <view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
</view> <view class="picker-wrapper" @click="showProject=true">
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex" <text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
@change="handleProjectChange">
<view class="picker-wrapper">
<text class="picker-text">{{ selectedProjectName }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon> <u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view> </view>
</picker>
</view> </view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
:columns="projectOptions"></up-picker>
<!-- 历组件 --> <!-- 期选择 -->
<view class="calendar-wrapper" v-if="selectedProjectId"> <view class="date-filter" style="margin: 20rpx 20rpx 0;" v-if="selectProcet">
<uni-calendar :insert="true" :range='true' :lunar="false" :show-month="true" :selected="selectedDates" <view class="date-picker" @click="openCalendar()">
@monthSwitch="handleMonthSwitch" @change="handleDateChange"> <up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
</uni-calendar> <view class="date-text">
<text class="label">开始日期</text>
<text class="value">{{ begin || '请选择' }}</text>
</view>
</view>
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">结束日期</text>
<text class="value">{{ end || '请选择' }}</text>
</view>
</view>
</view> </view>
<!-- 学生选择区域 --> <!-- 学生选择区域 -->
<view class="select-section" v-if="selectedProjectId && selectedDate"> <view class="select-section" v-if="selectProcet && begin ">
<view class="select-label"> <view class="select-label">
<text class="label-text">选择学生</text> <text class="label-text">选择学生</text>
</view> </view>
@@ -51,7 +61,7 @@
</view> </view>
</view> </view>
<scroll-view class="student-list" scroll-y> <scroll-view class="student-list" scroll-y>
<view v-for="(student, index) in studentList" :key="student.id" class="student-item" <view v-for="(student, index) in studentList" :key="student.studentId" class="student-item"
:class="{ 'selected': selectedStudentIndexes.includes(index) }" :class="{ 'selected': selectedStudentIndexes.includes(index) }"
@click="toggleStudentSelection(index)"> @click="toggleStudentSelection(index)">
<view class="item-checkbox"> <view class="item-checkbox">
@@ -61,40 +71,29 @@
</view> </view>
</view> </view>
<view class="item-avatar"> <view class="item-avatar">
<view class="avatar-circle" <view class="avatar-circle male">
:class="{ 'male': student.gender === '男', 'female': student.gender === '女' }">
<text class="avatar-text">{{ student.name.charAt(0) }}</text> <text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view> </view>
</view> </view>
<view class="item-info"> <view class="item-info">
<text class="item-name">{{ student.name }}</text> <text class="item-name">{{ student.name }}</text>
<view class="item-meta">
<text class="gender-badge"
:class="{ 'male': student.gender === '男', 'female': student.gender === '女' }">
{{ student.gender }}
</text>
<text class="age-text">{{ student.age }}</text>
</view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<!-- 数据表格 --> <!-- 数据表格 -->
<view class="table-section" v-if="selectedDate && selectedStudentIndexes.length > 0"> <view v-if="tableData.length>0" class="table-section">
<sl-table :columns="columns" :tableData="tableData" @cell-click="handleCellClick"> <next-table :show-header="true" :columns="columns" :stripe="true" :fit="false" :show-summary='false'
<template #empty> :data="tableData" :showPaging='true' :pageIndex="page" @pageChange="pageChange"
<view class="empty-container"> :pageTotal="pageTotal"></next-table>
<view class="empty-icon">
<u-icon name="file-text" size="80" color="#ccc"></u-icon>
</view>
<text class="empty-text">该日期暂无训练数据</text>
</view>
</template>
</sl-table>
</view> </view>
<!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm"
@close="calendarClose">
</up-calendar>
</view> </view>
</template> </template>
@@ -102,6 +101,7 @@
import { onShow, onLoad } from "@dcloudio/uni-app" import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
interface Project { interface Project {
id : string id : string
@@ -134,65 +134,62 @@
const currentMonth = ref(new Date().getMonth() + 1) const currentMonth = ref(new Date().getMonth() + 1)
const selectedDate = ref('') const selectedDate = ref('')
// 项目选择
let showProject = ref(false)
let selectProcet = ref('')
let selectId = ref('')
// 日期选择
const begin = ref<string>('')
const end = ref<string>('')
const showCalendar = ref(false)
const calendarType = ref<'begin' | 'end'>('begin')
// 分页相关
let page = ref(1)
let status = ref('loadmore')
let pageTotal = ref(10)
const columns = ref([ const columns = ref([
{ {
label: '日期', label: '训练日期',
prop: 'date', name: 'addTime',
width: '100px'
}, },
{ {
label: '姓名', label: '姓名',
prop: 'name', name: 'studentName',
width: '100px'
}, },
{ {
label: '项目名称', label: '项目名称',
prop: 'projectName', name: 'name',
width: '120px'
}, },
{ {
label: '包干计划', label: '项目类型',
prop: 'plan', name: 'planType',
width: '100px'
}, },
{ {
label: '完成比例', label: '完成比例',
prop: 'completion', name: 'completion',
width: '100px'
}, },
{ {
label: '详细数据', label: '详细数据',
prop: 'detail', name: 'detail',
width: '120px'
} }
]) ])
const tableData = ref<TableDataItem[]>([]) const tableData = ref<TableDataItem[]>([])
const projectList = ref<Project[]>([
{ id: '1', name: '100米自由泳' },
{ id: '2', name: '200米自由泳' },
{ id: '3', name: '400米自由泳' },
{ id: '4', name: '100米蛙泳' }
])
const projectOptions = ref(projectList.value)
const projectOptions = ref<Array<any>>([[]])
const selectedProjectIndex = ref(-1) const selectedProjectIndex = ref(-1)
const selectedProjectId = ref('') const selectedProjectId = ref('')
const selectedProjectName = computed(() => {
if (selectedProjectIndex.value === -1) {
return '请选择项目'
}
return projectList.value[selectedProjectIndex.value]?.name || '请选择项目'
})
const selectedDates = ref([
{ date: '2026-03-28', info: '训练' },
{ date: '2026-03-29', info: '训练' },
{ date: '2026-03-30', info: '训练' }
])
const studentList = ref<Student[]>([]) const studentList = ref<Student[]>([])
@@ -215,69 +212,12 @@
return Math.round(total / tableData.value.length) return Math.round(total / tableData.value.length)
}) })
const mockData : Record<string, Record<string, TableDataItem[]>> = {
'1': {
'2026-03-28': [
{ name: '张小明', projectName: '100米自由泳', plan: '2000米', completion: 95, detail: '1900/2000' },
{ name: '李小红', projectName: '100米自由泳', plan: '2000米', completion: 92, detail: '1840/2000' },
{ name: '王小明', projectName: '100米自由泳', plan: '2000米', completion: 90, detail: '1800/2000' }
],
'2026-03-29': [
{ name: '张小明', projectName: '100米自由泳', plan: '2000米', completion: 90, detail: '1800/2000' },
{ name: '李小红', projectName: '100米自由泳', plan: '2000米', completion: 95, detail: '1900/2000' },
{ name: '王小明', projectName: '100米自由泳', plan: '2000米', completion: 88, detail: '1760/2000' }
]
},
'2': {
'2026-03-28': [
{ name: '张小明', projectName: '200米自由泳', plan: '1500米', completion: 88, detail: '1320/1500' },
{ name: '李小红', projectName: '200米自由泳', plan: '1500米', completion: 92, detail: '1380/1500' },
{ name: '王小明', projectName: '200米自由泳', plan: '1500米', completion: 85, detail: '1275/1500' }
],
'2026-03-29': [
{ name: '张小明', projectName: '200米自由泳', plan: '1500米', completion: 90, detail: '1350/1500' },
{ name: '李小红', projectName: '200米自由泳', plan: '1500米', completion: 88, detail: '1320/1500' }
]
},
'3': {
'2026-03-28': [
{ name: '张小明', projectName: '400米自由泳', plan: '1200米', completion: 85, detail: '1020/1200' },
{ name: '李小红', projectName: '400米自由泳', plan: '1200米', completion: 90, detail: '1080/1200' }
]
},
'4': {
'2026-03-28': [
{ name: '赵小芳', projectName: '100米蛙泳', plan: '1800米', completion: 88, detail: '1584/1800' },
{ name: '陈小' + '刚', projectName: '100米蛙泳', plan: '1800米', completion: 95, detail: '1710/1800' }
]
}
}
const mockStudents : Record<string, Student[]> = {
'1': [
{ id: 's1', name: '张小明', gender: '男', age: 12 },
{ id: 's2', name: '李小红', gender: '女', age: 11 },
{ id: 's3', name: '王小明', gender: '男', age: 13 }
],
'2': [
{ id: 's1', name: '张小明', gender: '男', age: 12 },
{ id: 's2', name: '李小红', gender: '女', age: 11 },
{ id: 's3', name: '王小明', gender: '男', age: 13 }
],
'3': [
{ id: 's1', name: '张小明', gender: '男', age: 12 },
{ id: 's2', name: '李小红', gender: '女', age: 11 }
],
'4': [
{ id: 's4', name: '赵小芳', gender: '女', age: 10 },
{ id: 's5', name: '陈小' + '刚', gender: '男', age: 14 }
]
}
onLoad(() => { onLoad(() => {
loadData() loadData()
getProjectData()
}) })
onShow(() => { onShow(() => {
@@ -288,50 +228,53 @@
} }
const handleCellClick = (event : any) => {
console.log('单元格点击事件:', event) const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => {
if (res.code == 0) {
projectOptions.value[0] = res.data.list
} else {
Service.Msg(res.msg)
}
})
} }
const handleProjectChange = (e : any) => { const selectProcetFunc = (e : any) => {
const index = e.detail.value selectProcet.value = e.value[0].name
selectedProjectIndex.value = index selectId.value = e.value[0].planId
const selectedProject = projectList.value[index] getProjectDetail()
if (selectedProject) {
selectedProjectId.value = selectedProject.id
selectedDate.value = ''
selectedStudentIndexes.value = []
selectedStudentIds.value = []
tableData.value = []
loadProjectStudents(selectedProject.id)
}
} }
const handleMonthSwitch = (e : any) => {
currentYear.value = e.year const getProjectDetail = () => {
currentMonth.value = e.month PlanService.GetPlanInfo(selectId.value).then(res => {
loadData() if (res.code == 0) {
selectedDate.value = '' studentList.value = res.data.plan.users
selectedStudentIndexes.value = [] } else {
selectedStudentIds.value = [] Service.Msg(res.msg)
tableData.value = [] }
})
} }
const handleDateChange = (e : any) => {
console.log(e);
const date = e.fulldate
selectedDate.value = date const openCalendar = () => {
selectedStudentIndexes.value = [] showCalendar.value = true
selectedStudentIds.value = []
tableData.value = []
} }
const loadProjectStudents = (projectId : string) => { const calendarConfirm = (e : any) => {
const students = mockStudents[projectId] || [] begin.value = e[0]
studentList.value = students end.value = e[e.length - 1]
console.log(`项目 ${projectId} 的学生列表加载完成,共 ${students.length} 位学生`) showCalendar.value = false
} }
const calendarClose = () => {
showCalendar.value = false
}
const selectAllStudents = () => { const selectAllStudents = () => {
selectedStudentIndexes.value = studentList.value.map((_, index) => index) selectedStudentIndexes.value = studentList.value.map((_, index) => index)
} }
@@ -350,36 +293,50 @@
} }
const confirmStudentSelection = () => { const confirmStudentSelection = () => {
selectedStudentIds.value = selectedStudentIndexes.value.map(index => studentList.value[index].id) selectedStudentIds.value = selectedStudentIndexes.value.map(index => studentList.value[index].studentId)
showStudentPicker.value = false showStudentPicker.value = false
filterAndLoadData()
getRecord()
} }
const getRecord = () => {
page.value = 1
getRecordList()
}
const getRecordList = () => {
let studentIdList = selectedStudentIndexes.value.map(index => {
return studentList.value[index].studentId
})
let data = {
planId: selectId.value,
studentId: JSON.stringify(studentIdList),
sTime: begin.value,
eTime: end.value,
page: page.value
}
PlanService.GetBaoganLog(data).then(res => {
if (res.code == 0) {
pageTotal.value = res.data.pageTotal
tableData.value = res.data.list
} else {
Service.Msg(res.msg)
}
})
}
const pageChange = (e) => {
page.value = e
getRecordList()
}
const closeStudentPicker = () => { const closeStudentPicker = () => {
showStudentPicker.value = false showStudentPicker.value = false
} }
const filterAndLoadData = () => {
if (!selectedDate.value || selectedStudentIndexes.value.length === 0) {
tableData.value = []
return
}
const projectData = mockData[selectedProjectId.value]
if (!projectData) {
tableData.value = []
return
}
const dateData = projectData[selectedDate.value]
if (!dateData) {
tableData.value = []
return
}
const selectedStudentNames = selectedStudentIndexes.value.map(index => studentList.value[index].name)
tableData.value = dateData.filter(item => selectedStudentNames.includes(item.name))
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -393,7 +350,7 @@
} }
.header-section { .header-section {
background-color: #fff;
padding: 32rpx 28rpx 24rpx; padding: 32rpx 28rpx 24rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
@@ -413,6 +370,37 @@
} }
} }
.date-filter {
display: flex;
gap: 24rpx;
.date-picker {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
.date-text {
.label {
display: block;
font-size: 26rpx;
color: #999;
}
.value {
display: block;
font-size: 30rpx;
color: #333;
margin-top: 4rpx;
font-weight: 600;
}
}
}
}
.select-section { .select-section {
background-color: #fff; background-color: #fff;
margin: 0 20rpx 20rpx; margin: 0 20rpx 20rpx;
@@ -455,14 +443,6 @@
} }
} }
.calendar-wrapper {
background-color: #fff;
margin: 0 20rpx 20rpx;
padding: 20rpx 0;
border-radius: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.student-picker-modal { .student-picker-modal {
position: fixed; position: fixed;
left: 0; left: 0;

View File

@@ -8,41 +8,34 @@
</view> </view>
</view> </view>
<!-- ==================== 项目选择区域 ==================== --> <!-- 项目选择区域 -->
<view class="project-select-section"> <view class="select-section">
<view class="section-header"> <view class="select-label">
<text class="section-title">选择项目</text> <view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
</view> <view class="picker-wrapper" @click="showProject=true">
<!-- 项目选择器 --> <text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex"
@change="handleProjectChange">
<view class="picker-wrapper">
<text class="picker-text">{{ selectedProjectName }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon> <u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view> </view>
</picker>
</view> </view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
:columns="projectOptions"></up-picker>
<!-- ==================== 排名列表区域 ==================== --> <!-- ==================== 排名列表区域 ==================== -->
<!-- 仅在选择了项目且有数据时显示 --> <!-- 仅在选择了项目且有数据时显示 -->
<view class="ranking-section" v-if="selectedProjectId && gradeList.length > 0"> <view class="ranking-section" v-if="selectProcet && gradeList.length > 0">
<!-- 排名统计卡片 --> <!-- 排名统计卡片 -->
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">对比项目</text> <text class="stat-label">对比项目</text>
<text class="stat-value">{{ selectedProjectName }}</text> <text class="stat-value">{{ selectProcet }}</text>
</view> </view>
<view class="stat-divider"></view> <view class="stat-divider"></view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">参与人数</text> <text class="stat-label">参与人数</text>
<text class="stat-value">{{ gradeList.length }}</text> <text class="stat-value">{{ gradeList.length }}</text>
</view> </view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">最高速度</text>
<text class="stat-value">{{ maxSpeed }}m/s</text>
</view>
</view> </view>
<!-- 排名列表 --> <!-- 排名列表 -->
@@ -56,14 +49,13 @@
<!-- 学生信息 --> <!-- 学生信息 -->
<view class="student-info"> <view class="student-info">
<text class="student-name">{{ item.name }}</text> <text class="student-name">{{ item.studentName }}</text>
<text class="student-speed">最快速度{{ item.bestSpeed }}m/s</text> <text class="student-speed">最快用时{{ item.quicklyTime }}s</text>
</view> </view>
<!-- 成绩详情 --> <!-- 成绩详情 -->
<view class="speed-detail"> <view class="speed-detail">
<text class="detail-text">用时{{ item.bestTime }}s</text> <text class="detail-text">日期{{ Service.formatDate(item.addTime,2)}}</text>
<text class="detail-text">日期{{ item.recordDate }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -75,33 +67,16 @@
import { onShow, onLoad } from "@dcloudio/uni-app" import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
// ==================== 响应式数据 - 项目相关 ==================== // ==================== 响应式数据 - 项目相关 ====================
// 项目列表数据 // 项目选择
const projectList = ref([ let showProject = ref(false)
{ id: '1', name: '100米自由泳' }, let selectProcet = ref('')
{ id: '2', name: '50米自由泳' }, let selectId = ref('')
{ id: '3', name: '200米自由泳' },
{ id: '4', name: '100米蛙泳' }
])
// 项目选择器选项(用于 picker 组件) const projectOptions = ref<Array<any>>([[]])
const projectOptions = ref(projectList.value)
// 当前选中的项目索引(用于 picker 组件的显示)
const selectedProjectIndex = ref(-1)
// 当前选中的项目 ID
const selectedProjectId = ref('')
// 当前选中的项目名称(用于显示)
const selectedProjectName = computed(() => {
if (selectedProjectIndex.value === -1) {
return '请选择项目'
}
return projectList.value[selectedProjectIndex.value]?.name || '请选择项目'
})
// ==================== TypeScript 接口定义 ==================== // ==================== TypeScript 接口定义 ====================
@@ -129,36 +104,7 @@
return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2) return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2)
}) })
// ==================== 模拟数据 ====================
/**
* 模拟的学生成绩数据(按项目分组)
* 键为项目 ID值为该项目的学生成绩列表
*/
const mockStudentGrades : Record<string, StudentGrade[]> = {
'1': [
{ id: 'g1', name: '王小明', bestSpeed: 3.89, bestTime: 25.72, recordDate: '03-24', studentId: 's3' },
{ id: 'g2', name: '张小明', bestSpeed: 3.97, bestTime: 25.18, recordDate: '03-24', studentId: 's1' },
{ id: 'g3', name: '赵小芳', bestSpeed: 3.03, bestTime: 32.96, recordDate: '03-14', studentId: 's4' },
{ id: 'g4', name: '李小红', bestSpeed: 3.38, bestTime: 29.58, recordDate: '03-24', studentId: 's2' },
{ id: 'g5', name: '陈小刚', bestSpeed: 2.82, bestTime: 35.46, recordDate: '03-19', studentId: 's5' }
],
'2': [
{ id: 'g6', name: '杨小龙', bestSpeed: 2.05, bestTime: 24.39, recordDate: '03-19', studentId: 's8' },
{ id: 'g7', name: '刘小华', bestSpeed: 2.11, bestTime: 23.70, recordDate: '03-19', studentId: 's6' },
{ id: 'g8', name: '张小丽', bestSpeed: 1.91, bestTime: 26.18, recordDate: '03-16', studentId: 's7' }
],
'3': [
{ id: 'g9', name: '吴小西', bestSpeed: 1.59, bestTime: 125.79, recordDate: '03-16', studentId: 's10' },
{ id: 'g10', name: '黄小东', bestSpeed: 1.61, bestTime: 124.23, recordDate: '03-14', studentId: 's9' },
{ id: 'g11', name: '周小南', bestSpeed: 0.00, bestTime: 0, recordDate: '-', studentId: 's11' },
{ id: 'g12', name: '徐小北', bestSpeed: 0.00, bestTime: 0, recordDate: '-', studentId: 's12' }
],
'4': [
{ id: 'g13', name: '马小兵', bestSpeed: 1.22, bestTime: 81.97, recordDate: '03-14', studentId: 's13' },
{ id: 'g14', name: '朱小红', bestSpeed: 1.13, bestTime: 88.47, recordDate: '03-12', studentId: 's14' }
]
}
// ==================== 生命周期钩子 ==================== // ==================== 生命周期钩子 ====================
@@ -168,56 +114,27 @@
*/ */
onLoad(() => { onLoad(() => {
// 初始化数据 // 初始化数据
loadProjectData() getProjectData()
}) })
/**
* 页面显示时触发
* 每次页面从后台切换到前台时执行
*/
onShow(() => {
// TODO: 如果需要在页面显示时刷新数据,可以在这里添加逻辑
})
// ==================== 业务逻辑方法 - 项目相关 ==================== // ==================== 业务逻辑方法 - 项目相关 ====================
/** const getProjectData = () => {
* 加载项目数据 PlanService.GetPlanListNoPage('计时项目').then(res => {
* 从后端 API 获取项目列表 if (res.code == 0) {
*/ projectOptions.value[0] = res.data.list
const loadProjectData = () => { } else {
// TODO: 调用后端 API 获取项目列表 Service.Msg(res.msg)
// 示例代码: }
// Service.Request('/api/projects', 'GET').then(res => { })
// projectList.value = res.data
// projectOptions.value = res.data
// })
} }
/** const selectProcetFunc = (e : any) => {
* 处理项目选择变化事件 selectProcet.value = e.value[0].name
* 当用户选择项目时触发 selectId.value = e.value[0].planId
* @param e picker 选择事件对象,包含 detail.value 属性表示选中的索引 loadProjectGrades(selectId.value)
*/
const handleProjectChange = (e : any) => {
// 获取选中的项目索引
const index = e.detail.value
// 更新选中项目的索引
selectedProjectIndex.value = index
// 获取选中的项目
const selectedProject = projectList.value[index]
if (selectedProject) {
// 更新选中项目的 ID
selectedProjectId.value = selectedProject.id
// 加载该项目的排名数据
loadProjectGrades(selectedProject.id)
}
// TODO: 实际项目中应该调用 API 获取该项目的排名数据
} }
/** /**
@@ -226,13 +143,13 @@
* @param projectId 项目 ID * @param projectId 项目 ID
*/ */
const loadProjectGrades = (projectId : string) => { const loadProjectGrades = (projectId : string) => {
// 从模拟数据中获取排名列表 PlanService.GetRankData(projectId).then(res => {
if (mockStudentGrades[projectId]) { if (res.code == 0) {
// 按最快速度从高到低排序 gradeList.value=res.data.list
gradeList.value = [...mockStudentGrades[projectId]].sort((a, b) => b.bestSpeed - a.bestSpeed)
} else { } else {
gradeList.value = [] Service.Msg(res.msg)
} }
})
} }
// ==================== 业务逻辑方法 - 排名相关 ==================== // ==================== 业务逻辑方法 - 排名相关 ====================
@@ -284,7 +201,7 @@
} }
/* ==================== 项目选择区域 ==================== */ /* ==================== 项目选择区域 ==================== */
.project-select-section { .select-section {
background-color: #fff; background-color: #fff;
margin: 0 20rpx 20rpx; margin: 0 20rpx 20rpx;
border-radius: 20rpx; border-radius: 20rpx;
@@ -292,17 +209,15 @@
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease;
&::before { .select-label {
content: ''; margin-bottom: 16rpx;
position: absolute;
left: 0; .label-text {
top: 0; font-size: 26rpx;
bottom: 0; font-weight: 600;
width: 6rpx; color: #666;
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%); }
border-radius: 20rpx 0 0 20rpx;
} }
.picker-wrapper { .picker-wrapper {
@@ -316,8 +231,8 @@
transition: all 0.3s ease; transition: all 0.3s ease;
&:active { &:active {
background-color: #fff0f5; background-color: #f0f0f0;
border-color: #1890ff; border-color: #faad14;
transform: scale(0.98); transform: scale(0.98);
} }

View File

@@ -7,53 +7,62 @@
</view> </view>
</view> </view>
<!-- 项目选择区域 -->
<view class="select-section"> <view class="select-section">
<view class="select-label"> <view class="select-label">
<text class="label-text">选择项目</text> <view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
</view> <view class="picker-wrapper" @click="showProject=true">
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex" <text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
@change="handleProjectChange">
<view class="picker-wrapper">
<text class="picker-text">{{ selectedProjectName }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon> <u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view> </view>
</picker> </view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
:columns="projectOptions"></up-picker>
<!-- 日期选择 -->
<view class="date-filter" style="margin: 20rpx 20rpx 0;" v-if="selectProcet">
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">开始日期</text>
<text class="value">{{ begin || '请选择' }}</text>
</view>
</view>
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">结束日期</text>
<text class="value">{{ end || '请选择' }}</text>
</view>
</view>
</view> </view>
<view class="calendar-wrapper" v-if="selectedProjectId"> <!-- 学生选择区域 -->
<uni-calendar <view class="select-section" v-if="selectProcet && begin ">
:insert="true"
:lunar="false"
:show-month="true"
:selected="selectedDates"
@monthSwitch="handleMonthSwitch"
@change="handleDateChange">
</uni-calendar>
</view>
<view class="select-section" v-if="selectedProjectId && selectedDate">
<view class="select-label"> <view class="select-label">
<text class="label-text">选择学生</text> <text class="label-text">选择学生</text>
</view> </view>
<view class="picker-wrapper" @click="showStudentSelect = true"> <view class="picker-wrapper" @click="showStudentPicker = true">
<text class="picker-text">{{ selectedStudentDisplay }}</text> <text class="picker-text">{{ selectedStudentDisplay }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon> <u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view> </view>
</view> </view>
<view class="modal-overlay" v-if="showStudentSelect" @click="closeStudentSelect"></view> <!-- 学生多选选择器 -->
<view class="student-picker-modal" v-if="showStudentSelect"> <view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
<view class="student-picker-modal" v-if="showStudentPicker">
<view class="picker-header"> <view class="picker-header">
<text class="header-title">选择学生</text> <text class="header-title">选择学生</text>
<view class="header-actions"> <view class="header-actions">
<text class="action-btn" @click="selectAllStudents">全选</text> <text class="action-btn" @click="selectAllStudents">全选</text>
<text class="action-btn primary" @click="confirmStudentSelect">确定</text> <text class="action-btn primary" @click="confirmStudentSelection">确定</text>
</view> </view>
</view> </view>
<scroll-view class="student-list" scroll-y> <scroll-view class="student-list" scroll-y>
<view v-for="(student, index) in studentList" :key="student.id" class="student-item" <view v-for="(student, index) in studentList" :key="student.studentId" class="student-item"
:class="{ 'selected': selectedStudentIndexes.includes(index) }" :class="{ 'selected': selectedStudentIndexes.includes(index) }"
@click="toggleStudentSelect(index)"> @click="toggleStudentSelection(index)">
<view class="item-checkbox"> <view class="item-checkbox">
<view class="checkbox-inner" :class="{ 'checked': selectedStudentIndexes.includes(index) }"> <view class="checkbox-inner" :class="{ 'checked': selectedStudentIndexes.includes(index) }">
<u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="14" <u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="14"
@@ -61,58 +70,62 @@
</view> </view>
</view> </view>
<view class="item-avatar"> <view class="item-avatar">
<view class="avatar-circle" <view class="avatar-circle male">
:class="{ 'male': student.gender === '男', 'female': student.gender === '女' }">
<text class="avatar-text">{{ student.name.charAt(0) }}</text> <text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view> </view>
</view> </view>
<view class="item-info"> <view class="item-info">
<text class="item-name">{{ student.name }}</text> <text class="item-name">{{ student.name }}</text>
<view class="item-meta">
<text class="gender-badge"
:class="{ 'male': student.gender === '男', 'female': student.gender === '女' }">
{{ student.gender }}
</text>
<text class="age-text">{{ student.age }}</text>
</view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<view v-if="tableData.length>0" class="table-section">
<view class="table-section" v-if="selectedDate && selectedStudentIndexes.length > 0 && tableData.length > 0"> <next-table :show-header="true" :columns="columns" :stripe="true" :fit="false" :show-summary='false'
<view class="table-wrapper-scroll"> :data="tableData" :showPaging='true' :pageIndex="page" @pageChange="pageChange" @cellClick="cellClick"
<view class="table-header"> :pageTotal="pageTotal"></next-table>
<view class="header-row">
<view class="header-cell" style="width: 100px;">日期</view>
<view class="header-cell" style="width: 100px;">姓名</view>
<view class="header-cell" style="width: 120px;">项目</view>
<view class="header-cell" style="width: 100px;">20</view>
<view class="header-cell" style="width: 100px;">70</view>
<view class="header-cell" style="width: 100px;">120</view>
<view class="header-cell" style="width: 100px;">170</view>
</view>
</view>
<view class="table-body">
<view v-for="(row, rowIndex) in tableData" :key="rowIndex" class="body-row" :class="{ 'even': rowIndex % 2 === 0, 'odd': rowIndex % 2 === 1 }">
<view class="body-cell" style="width: 100px;">{{ row.date }}</view>
<view class="body-cell" style="width: 100px;">{{ row.name }}</view>
<view class="body-cell" style="width: 120px;">{{ row.projectName }}</view>
<view class="body-cell" style="width: 100px;">{{ row.segment20 }}</view>
<view class="body-cell" style="width: 100px;">{{ row.segment70 }}</view>
<view class="body-cell" style="width: 100px;">{{ row.segment120 }}</view>
<view class="body-cell" style="width: 100px;">{{ row.segment170 }}</view>
</view>
</view>
</view>
</view> </view>
<view class="empty-container" v-if="selectedDate && selectedStudentIndexes.length > 0 && tableData.length === 0">
<view class="empty-container" v-if="begin && selectedStudentIndexes.length > 0 && tableData.length === 0">
<view class="empty-icon"> <view class="empty-icon">
<u-icon name="file-text" size="80" color="#ccc"></u-icon> <u-icon name="file-text" size="80" color="#ccc"></u-icon>
</view> </view>
<text class="empty-text">该日期暂无分段数据</text> <text class="empty-text">该日期暂无分段数据</text>
</view> </view>
<!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm"
@close="calendarClose">
</up-calendar>
<!-- 分段详情弹窗 -->
<up-popup :show="showSegmentPopup" @close="closeSegmentPopup" @open="openSegmentPopup" mode="center" round="16" bgColor="#fff">
<view class="segment-popup-content">
<view class="popup-header">
<text class="popup-title">分段详情</text>
<u-icon name="close" size="22" color="#999" @click="closeSegmentPopup"></u-icon>
</view>
<view class="popup-table">
<view class="popup-table-header">
<text class="popup-th">分段数</text>
<text class="popup-th">距离</text>
<text class="popup-th">时间</text>
</view>
<scroll-view class="popup-scroll" scroll-y>
<view v-for="(item, index) in row.split('/')" :key="index" class="popup-table-row">
<text class="popup-td">{{ index+1 }}</text>
<text class="popup-td">{{ item.split('-')[0] }}</text>
<text class="popup-td">{{ item.split('-')[1] }}</text>
</view>
<view v-if="row.split('/').length === 0" class="popup-empty">
<text class="popup-empty-text">暂无分段数据</text>
</view>
</scroll-view>
</view>
</view>
</up-popup>
</view> </view>
</template> </template>
@@ -120,14 +133,10 @@
import { onShow, onLoad } from "@dcloudio/uni-app" import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
interface Project {
id: string
name: string
}
interface Student { interface Student {
id: string studentId : string
name : string name : string
gender : string gender : string
age : number age : number
@@ -143,40 +152,24 @@
segment170 : string segment170 : string
} }
const currentYear = ref(new Date().getFullYear()) let row=ref('')
const currentMonth = ref(new Date().getMonth() + 1)
const selectedDate = ref('')
const projectList = ref<Project[]>([ // 项目选择
{ id: '1', name: '100米自由泳' }, let showProject = ref(false)
{ id: '2', name: '200米自由泳' }, let selectProcet = ref('')
{ id: '3', name: '50米蛙泳' }, let selectId = ref('')
{ id: '4', name: '100米蝶泳' }
])
const projectOptions = ref(projectList.value) // 日期选择
const selectedProjectIndex = ref(-1) const begin = ref<string>('')
const selectedProjectId = ref('') const end = ref<string>('')
const showCalendar = ref(false)
const selectedProjectName = computed(() => { const projectOptions = ref<Array<any>>([[]])
if (selectedProjectIndex.value === -1) {
return '请选择项目'
}
return projectList.value[selectedProjectIndex.value]?.name || '请选择项目'
})
const selectedDates = ref([
{ date: '2026-03-12', info: '分段' },
{ date: '2026-03-14', info: '分段' },
{ date: '2026-03-16', info: '分段' },
{ date: '2026-03-19', info: '分段' },
{ date: '2026-03-24', info: '分段' }
])
const studentList = ref<Student[]>([]) const studentList = ref<Student[]>([])
const selectedStudentIndexes = ref<number[]>([]) const selectedStudentIndexes = ref<number[]>([])
const selectedStudentIds = ref<string[]>([]) const selectedStudentIds = ref<string[]>([])
const showStudentSelect = ref(false) const showStudentPicker = ref(false)
const selectedStudentDisplay = computed(() => { const selectedStudentDisplay = computed(() => {
if (selectedStudentIndexes.value.length === 0) { if (selectedStudentIndexes.value.length === 0) {
@@ -187,110 +180,98 @@
} }
}) })
let page = ref(1)
let status = ref('loadmore')
let pageTotal = ref(10)
const columns = ref([
{
label: '训练日期',
name: 'addTime',
},
{
label: '姓名',
name: 'name',
},
{
label: '总时长',
name: 'time',
},
{
label: '详细数据',
name: 'subsection',
}
])
const tableData = ref<TableDataItem[]>([]) const tableData = ref<TableDataItem[]>([])
const mockData: Record<string, Record<string, any[]>> = {
'1': {
'2026-03-12': [
{ name: '王小明', projectName: '100米自由泳', segment20: 0.037, segment70: 0.128, segment120: 0.219, segment170: 0.311 },
{ name: '张小红', projectName: '100米自由泳', segment20: 0.042, segment70: 0.145, segment120: 0.248, segment170: 0.352 },
{ name: '李小龙', projectName: '100米自由泳', segment20: 0.032, segment70: 0.112, segment120: 0.192, segment170: 0.272 }
],
'2026-03-14': [
{ name: '王小明', projectName: '100米自由泳', segment20: 0.035, segment70: 0.122, segment120: 0.209, segment170: 0.296 },
{ name: '张小红', projectName: '100米自由泳', segment20: 0.040, segment70: 0.138, segment120: 0.236, segment170: 0.334 }
]
},
'2': {
'2026-03-16': [
{ name: '赵小芳', projectName: '200米自由泳', segment20: 0.038, segment70: 0.133, segment120: 0.228, segment170: 0.323 }
]
},
'3': {
'2026-03-19': [
{ name: '陈小刚', projectName: '50米蛙泳', segment20: 0.044, segment70: 0.154, segment120: 0.264, segment170: 0.374 }
]
},
'4': {
'2026-03-24': [
{ name: '周小丽', projectName: '100米蝶泳', segment20: 0.041, segment70: 0.143, segment120: 0.245, segment170: 0.347 }
]
}
}
const mockStudents: Record<string, Student[]> = {
'1': [
{ id: 's1', name: '王小明', gender: '男', age: 12 },
{ id: 's2', name: '张小红', gender: '女', age: 11 },
{ id: 's3', name: '李小龙', gender: '男', age: 13 }
],
'2': [
{ id: 's4', name: '赵小芳', gender: '女', age: 10 }
],
'3': [
{ id: 's5', name: '陈小刚', gender: '男', age: 14 }
],
'4': [
{ id: 's6', name: '周小丽', gender: '女', age: 12 }
]
}
onLoad(() => { onLoad(() => {
loadData() getProjectData()
}) })
onShow(() => { onShow(() => {
}) })
const loadData = () => { const getProjectData = () => {
PlanService.GetPlanListNoPage('分段项目').then(res => {
if (res.code == 0) {
projectOptions.value[0] = res.data.list
} else {
Service.Msg(res.msg)
}
})
} }
const handleProjectChange = (e: any) => { const selectProcetFunc = (e : any) => {
const index = e.detail.value selectProcet.value = e.value[0].name
selectedProjectIndex.value = index selectId.value = e.value[0].planId
const selectedProject = projectList.value[index] begin.value = ''
end.value = ''
if (selectedProject) {
selectedProjectId.value = selectedProject.id
selectedDate.value = ''
selectedStudentIndexes.value = [] selectedStudentIndexes.value = []
selectedStudentIds.value = [] selectedStudentIds.value = []
tableData.value = [] tableData.value = []
loadProjectStudents(selectedProject.id) loadProjectStudents(selectId.value)
}
} }
const handleMonthSwitch = (e: any) => { const openCalendar = () => {
currentYear.value = e.year showCalendar.value = true
currentMonth.value = e.month }
loadData()
selectedDate.value = '' const calendarConfirm = (e : any) => {
begin.value = e[0]
end.value = e[e.length - 1]
showCalendar.value = false
selectedStudentIndexes.value = [] selectedStudentIndexes.value = []
selectedStudentIds.value = [] selectedStudentIds.value = []
tableData.value = [] tableData.value = []
} }
const handleDateChange = (e: any) => { const calendarClose = () => {
const date = e.fulldate showCalendar.value = false
selectedDate.value = date
selectedStudentIndexes.value = []
selectedStudentIds.value = []
tableData.value = []
} }
const loadProjectStudents = (projectId : string) => { const loadProjectStudents = (projectId : string) => {
const students = mockStudents[projectId] || [] PlanService.GetPlanInfo(projectId).then(res => {
studentList.value = students if (res.code == 0) {
console.log(`项目 ${projectId} 的学生列表加载完成,共 ${students.length} 位学生`) studentList.value = res.data.plan.users
} else {
Service.Msg(res.msg)
}
})
} }
const selectAllStudents = () => { const selectAllStudents = () => {
selectedStudentIndexes.value = studentList.value.map((_, index) => index) selectedStudentIndexes.value = studentList.value.map((_, index) => index)
} }
const toggleStudentSelect = (index: number) => { const toggleStudentSelection = (index : number) => {
const idx = selectedStudentIndexes.value.indexOf(index) const idx = selectedStudentIndexes.value.indexOf(index)
if (idx > -1) { if (idx > -1) {
selectedStudentIndexes.value.splice(idx, 1) selectedStudentIndexes.value.splice(idx, 1)
@@ -299,46 +280,71 @@
} }
} }
const confirmStudentSelect = () => { const confirmStudentSelection = () => {
selectedStudentIds.value = selectedStudentIndexes.value.map(index => studentList.value[index].id) selectedStudentIds.value = selectedStudentIndexes.value.map(index => studentList.value[index].studentId)
showStudentSelect.value = false showStudentPicker.value = false
filterAndLoadData() getRecord()
} }
const closeStudentSelect = () => { const closeStudentPicker = () => {
showStudentSelect.value = false showStudentPicker.value = false
} }
const filterAndLoadData = () => { const getRecord = () => {
if (!selectedDate.value || selectedStudentIndexes.value.length === 0) { page.value = 1
tableData.value = [] getRecordList()
return
} }
const projectData = mockData[selectedProjectId.value] const getRecordList = () => {
if (!projectData) { let studentIdList = selectedStudentIndexes.value.map(index => {
tableData.value = [] return studentList.value[index].studentId
return })
let data = {
planId: selectId.value,
studentId: JSON.stringify(studentIdList),
sTime: begin.value,
eTime: end.value,
page: page.value
}
PlanService.GetFenduanLog(data).then(res => {
if (res.code == 0) {
pageTotal.value = res.data.pageTotal
tableData.value = res.data.list
} else {
Service.Msg(res.msg)
}
})
} }
const dateData = projectData[selectedDate.value]
if (!dateData) {
tableData.value = [] const pageChange = (e) => {
return page.value = e
getRecordList()
} }
const selectedStudentNames = selectedStudentIndexes.value.map(index => studentList.value[index].name) const showSegmentPopup = ref(false)
const filteredData = dateData.filter(item => selectedStudentNames.includes(item.name))
tableData.value = filteredData.map(item => ({ interface SegmentItem {
date: selectedDate.value, index: number
name: item.name, distance: string
projectName: item.projectName, time: string
segment20: item.segment20.toFixed(3), }
segment70: item.segment70.toFixed(3),
segment120: item.segment120.toFixed(3), const segmentPopupData = ref<SegmentItem[]>([])
segment170: item.segment170.toFixed(3)
})) const openSegmentPopup = () => {
showSegmentPopup.value = true
}
const closeSegmentPopup = () => {
showSegmentPopup.value = false
}
const cellClick = (rows: any) => {
row.value=rows.subsection
openSegmentPopup()
} }
</script> </script>
@@ -374,6 +380,37 @@
} }
} }
.date-filter {
display: flex;
gap: 24rpx;
.date-picker {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
.date-text {
.label {
display: block;
font-size: 26rpx;
color: #999;
}
.value {
display: block;
font-size: 30rpx;
color: #333;
margin-top: 4rpx;
font-weight: 600;
}
}
}
}
/* ==================== 选择区域通用样式 ==================== */ /* ==================== 选择区域通用样式 ==================== */
.select-section { .select-section {
background-color: #fff; background-color: #fff;
@@ -407,7 +444,7 @@
&:active { &:active {
background-color: #f0f0f0; background-color: #f0f0f0;
border-color: #1890ff; border-color: #faad14;
transform: scale(0.98); transform: scale(0.98);
} }
@@ -418,16 +455,6 @@
} }
} }
/* ==================== 日历组件包装 ==================== */
.calendar-wrapper {
background-color: #fff;
margin: 0 20rpx 20rpx;
padding: 20rpx 0;
border-radius: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
/* ==================== 学生选择器弹窗 ==================== */
.student-picker-modal { .student-picker-modal {
position: fixed; position: fixed;
left: 0; left: 0;
@@ -455,7 +482,7 @@
.header-title { .header-title {
font-size: 36rpx; font-size: 36rpx;
font-weight: 700; font-weight: 700;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@@ -478,8 +505,8 @@
&.primary { &.primary {
color: #fff; color: #fff;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%); background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%);
box-shadow: 0 4rpx 16rpx rgba(24, 144, 255, 0.3); box-shadow: 0 4rpx 16rpx rgba(250, 173, 20, 0.3);
} }
&:active { &:active {
@@ -503,7 +530,7 @@
border-radius: 20rpx; border-radius: 20rpx;
margin-top: 16rpx; margin-top: 16rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -528,9 +555,9 @@
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.checked { &.checked {
border-color: #1890ff; border-color: #faad14;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3); box-shadow: 0 2rpx 8rpx rgba(250, 173, 20, 0.3);
} }
} }
} }
@@ -553,7 +580,7 @@
transition: all 0.3s ease; transition: all 0.3s ease;
&.male { &.male {
background: linear-gradient(135deg, #1890ffkt 0%, #40a9ff 50%, #096dd9 100%); background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
} }
&.female { &.female {
@@ -751,4 +778,79 @@
transform: translateY(0); transform: translateY(0);
} }
} }
.segment-popup-content {
width: 620rpx;
max-height: 70vh;
padding: 32rpx;
display: flex;
flex-direction: column;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.popup-title {
font-size: 34rpx;
font-weight: 700;
color: #333;
}
}
.popup-table {
background-color: #fafafa;
border-radius: 16rpx;
overflow: hidden;
display: flex;
flex-direction: column;
.popup-table-header {
display: flex;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
padding: 24rpx 0;
.popup-th {
flex: 1;
text-align: center;
font-size: 28rpx;
font-weight: 700;
color: #fff;
}
}
.popup-scroll {
max-height: 400rpx;
.popup-table-row {
display: flex;
padding: 24rpx 0;
border-bottom: 1rpx solid #eee;
background-color: #fff;
&:nth-child(even) {
background-color: #fafafa;
}
.popup-td {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #333;
}
}
.popup-empty {
padding: 60rpx 0;
text-align: center;
.popup-empty-text {
font-size: 28rpx;
color: #999;
}
}
}
}
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="timing-container"> <view class="baogan-container">
<!-- 页面标题区域 --> <!-- 页面标题区域 -->
<view class="header-section"> <view class="header-section">
<view class="header-title"> <view class="header-title">
@@ -8,57 +8,38 @@
</view> </view>
</view> </view>
<!-- 日历组件 -->
<view class="calendar-wrapper">
<uni-calendar <!-- 日期选择 -->
:insert="true" <view class="date-filter" style="margin: 20rpx 20rpx 0;">
:lunar="false" <view class="date-picker" @click="openCalendar()">
:show-month="true" <up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
:selected="selectedDates" <view class="date-text">
@monthSwitch="handleMonthSwitch" <text class="label">开始日期</text>
@change="handleDateChange"> <text class="value">{{ begin || '请选择' }}</text>
</uni-calendar> </view>
</view>
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">结束日期</text>
<text class="value">{{ end || '请选择' }}</text>
</view>
</view>
</view> </view>
<!-- 选中日期数据统计 -->
<!-- 仅在选择了日期时显示统计信息 -->
<view class="date-summary" v-if="selectedDate">
<view class="summary-item">
<text class="summary-label">选中日期</text>
<text class="summary-value">{{ selectedDate }}</text>
</view>
<view class="summary-divider"></view>
<view class="summary-item">
<text class="summary-label">训练人数</text>
<text class="summary-value">{{ tableData.length }}</text>
</view>
<view class="summary-divider"></view>
<view class="summary-item">
<text class="summary-label">平均速度</text>
<text class="summary-value">{{ averageSpeed }}m/s</text>
</view>
</view>
<!-- 数据表格 --> <!-- 数据表格 -->
<!-- 仅在选择了日期时显示表格 --> <view v-if="tableData.length>0" class="table-section">
<view class="table-section" v-if="selectedDate"> <next-table :show-header="true" :columns="columns" :stripe="true" :fit="false" :show-summary='false'
<sl-table :data="tableData" :showPaging='true' :pageIndex="page" @pageChange="pageChange"
:columns="columns" :pageTotal="pageTotal"></next-table>
:tableData="tableData"
@cell-click="handleCellClick">
<!-- 空数据插槽 -->
<template #empty>
<view class="empty-container">
<view class="empty-icon">
<u-icon name="file-text" size="80" color="#ccc"></u-icon>
</view>
<text class="empty-text">该日期暂无训练数据</text>
</view>
</template>
</sl-table>
</view> </view>
<!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm"
@close="calendarClose">
</up-calendar>
</view> </view>
</template> </template>
@@ -66,250 +47,143 @@
import { onShow, onLoad } from "@dcloudio/uni-app" import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
// ==================== 响应式数据定义 ==================== interface Project {
id : string
name : string
}
interface Student {
id : string
name : string
gender : string
age : number
}
interface TableDataItem {
date : string
name : string
projectName : string
plan : string
completion : number
detail : string
}
interface StudentRecord {
student : Student
date : string
segments : TableDataItem[]
}
// 当前年份,用于月份切换时记录当前年份
const currentYear = ref(new Date().getFullYear()) const currentYear = ref(new Date().getFullYear())
// 当前月份1-12用于月份切换时记录当前月份
const currentMonth = ref(new Date().getMonth() + 1) const currentMonth = ref(new Date().getMonth() + 1)
// 当前选中的日期,格式为 'YYYY-MM-DD'
// 空字符串表示未选择日期
const selectedDate = ref('') const selectedDate = ref('')
// ==================== 表格配置 ==================== // 项目选择
let showProject = ref(false)
let selectProcet = ref('')
let selectId = ref('')
// 日期选择
const begin = ref<string>('')
const end = ref<string>('')
const showCalendar = ref(false)
const calendarType = ref<'begin' | 'end'>('begin')
// 分页相关
let page = ref(1)
let status = ref('loadmore')
let pageTotal = ref(10)
// 表格列定义
// label: 列标题
// prop: 数据项中对应的字段名
// width: 列宽度
const columns = ref([ const columns = ref([
{ {
label: '姓名', label: '姓名',
prop: 'name', name: 'studentName',
width: '100px'
}, },
{ {
label: '项目名称', label: '项目名称',
prop: 'projectName', name: 'planName',
width: '150px'
}, },
{ {
label: '最速度', label: '最速度',
prop: 'maxSpeed', name: 'quicklyTime',
width: '120px'
}, },
{ {
label: '计时数据', label: '详细数据',
prop: 'timingData', name: 'time',
width: '150px' width:'200'
} }
]) ])
// ==================== TypeScript 接口定义 ====================
// 表格数据项接口定义
// 描述一条计时记录的数据结构
interface TableDataItem {
name: string // 学员姓名
projectName: string // 项目名称100米自由泳
maxSpeed: number // 最大速度单位m/s
timingData: string // 计时数据1.23, 1.25, 1.24
}
// ==================== 响应式数据 - 表格数据 ====================
// 表格数据列表
// 存储当前选中日期的所有计时记录
// 初始状态为空数组,等待用户选择日期后加载数据
const tableData = ref<TableDataItem[]>([]) const tableData = ref<TableDataItem[]>([])
// ==================== 响应式数据 - 日历标记 ====================
// 日历打点数据
// 用于在日历上标记有训练记录的日期
// date: 日期字符串,格式为 'YYYY-MM-DD'
// info: 显示在日期上的标记信息
const selectedDates = ref([
{ date: '2026-03-12', info: '训练' },
{ date: '2026-03-14', info: '训练' },
{ date: '2026-03-16', info: '训练' },
{ date: '2026-03-19', info: '训练' },
{ date: '2026-03-24', info: '训练' }
])
// ==================== 计算属性 ====================
// 平均速度计算属性
// 计算当前选中日期所有学员的平均最大速度
// 如果没有数据则返回 0
const averageSpeed = computed(() => {
// 如果没有数据,返回 0
if (tableData.value.length === 0) return 0
// 计算所有学员最大速度的总和
const total = tableData.value.reduce((sum, item) => sum + item.maxSpeed, 0)
// 计算平均值,保留两位小数
return (total / tableData.value.length).toFixed(2)
})
// ======================= 模拟数据 ====================
// 模拟的训练数据
// 键为日期字符串,值为该日期的计时记录列表
// 在实际项目中,这里应该从后端 API 获取数据
const mockData: Record<string, TableDataItem[]> = {
'2026-03-12': [
{ name: '张小明', projectName: '50米自由泳', maxSpeed: 2.15, timingData: '25.35s, 25.42s, 25.28s' },
{ name: '李小红', projectName: '50米自由泳', maxSpeed: 1.98, timingData: '27.15s, 27.32s, 27.08s' },
{ name: '王小明', projectName: '50米自由泳', maxSpeed: 2.22, timingData: '24.68s, 24.75s, 24.62s' }
],
'2026-03-14': [
{ name: '张小明', projectName: '50米自由泳', maxSpeed: 2.18, timingData: '25.12s, 25.18s, 25.08s' },
{ name: '李小红', projectName: '50米自由泳', maxSpeed: 2.02, timingData: '26.85s, 26.92s, 26.78s' },
{ name: '赵小芳', projectName: '50米自由泳', maxSpeed: 1.92, timingData: '27.95s, 28.12s, 27.88s' },
{ name: '王小明', projectName: '50米自由泳', maxSpeed: 2.25, timingData: '24.52s, 24.58s, 24.48s' }
],
'2026-03-16': [
{ name: '张小明', projectName: '50米自由泳', maxSpeed: 2.12, timingData: '25.55s, 25.62s, 25.48s' },
{ name: '李小红', projectName: '50米自由泳', maxSpeed: 2.05, timingData: '26.72s, 26.85s, 26.68s' }
],
'2026-03-19': [
{ name: '张小明', projectName: '50米自由泳', maxSpeed: 2.20, timingData: '24.98s, 25.05s, 24.92s' },
{ name: '李小红', projectName: '50米自由泳', maxSpeed: 2.08, timingData: '26.45s, 26.52s, 26.38s' },
{ name: '赵小芳', projectName: '50米自由泳', maxSpeed: 1.95, timingData: '27.75s, 27.88s, 27.65s' },
{ name: '王小明', projectName: '50米自由泳', maxSpeed: 2.28, timingData: '24.35s, 24.42s, 24.28s' },
{ name: '陈小刚', projectName: '50米自由泳', maxSpeed: 1.88, timingData: '28.25s, 28.38s, 28.15s' }
],
'2026-03-24': [
{ name: '张小明', projectName: '50米自由泳', maxSpeed: 2.15, timingData: '25.30s, 25.38s, 25.25s' },
{ name: '李小红', projectName: '50米自由泳', maxSpeed: 2.10, timingData: '26.25s, 26.32s, 26.18s' },
{ name: '赵小芳', projectName: '50米自由泳', maxSpeed: 1.98, timingData: '27.65s, 27.78s, 27.55s' }
]
}
// ==================== 生命周期钩子 ====================
// 页面加载时触发
// 在页面初始化时执行,只执行一次
onLoad(() => { onLoad(() => {
// 加载当前月份的数据
loadData()
}) })
// 页面显示时触发
// 每次页面从后台切换到前台时执行
// 可用于刷新页面显示的数据
onShow(() => { onShow(() => {
// TODO: 如果需要在页面显示时刷新数据,可以在这里添加逻辑
// 例如:调用接口获取最新数据
}) })
// ==================== 业务逻辑方法 ==================== const openCalendar = () => {
showCalendar.value = true
/**
* 加载数据
* 调用后端 API 获取数据
* 当前为模拟数据,实际开发中应替换为真实 API 调用
*/
const loadData = () => {
// TODO: 调用后端 API 获取数据
// 示例代码:
// Service.Request('/api/timing/data', 'GET', {
// year: currentYear.value,
// month: currentMonth.value
// }).then(res => {
// // 处理返回的数据
// })
} }
/** const calendarConfirm = (e : any) => {
* 处理表格单元格点击事件 begin.value = e[0]
* @param event 点击事件对象,包含行索引、列索引、单元格数据等信息 end.value = e[e.length - 1]
*/ showCalendar.value = false
const handleCellClick = (event: any) => { getRecord()
// 在控制台输出点击事件信息,用于调试
console.log('表格单元格点击事件:', event)
// TODO: 根据业务需求处理单元格点击
// 例如:点击某行可以查看详细信息,点击某列可以进行排序等
} }
/** const calendarClose = () => {
* 处理日历月份切换事件 showCalendar.value = false
* 当用户切换日历的月份时触发
* @param e 切换事件对象,包含 year 和 month 属性
*/
const handleMonthSwitch = (e: any) => {
// 更新当前年份
currentYear.value = e.year
// 更新当前月份
currentMonth.value = e.month
// 重新加载数据
// 获取切换后月份的训练记录和日历标记
loadData()
// 清空选中的日期
// 因为切换了月份,之前选择的日期可能不再显示
selectedDate.value = ''
// 清空表格数据
tableData.value = []
} }
/**
* 处理日历日期选择事件
* 当用户点击日历上的某个日期时触发
* @param e 选择事件对象fulldate 属性包含完整的日期字符串
*/
const handleDateChange = (e: any) => {
// 获取选择的完整日期,格式为 'YYYY-MM-DD'
const date = e.fulldate
// 更新选中的日期
selectedDate.value = date
// 根据选择的日期加载对应的训练数据
// 检查模拟数据中是否存在该日期的数据 const getRecord = () => {
if (mockData[date]) { page.value = 1
// 如果存在,加载数据 getRecordList()
tableData.value = mockData[date] }
const getRecordList = () => {
let data = {
sTime: begin.value,
eTime: end.value
}
PlanService.GetJishiLog(data).then(res => {
if (res.code == 0) {
pageTotal.value = res.data.pageTotal
tableData.value = res.data.list
} else { } else {
// 如果不存在,清空数据(表格将显示空状态) Service.Msg(res.msg)
tableData.value = [] }
})
} }
// TODO: 实际项目中应该调用 API 获取该日期的计时数据
// 示例代码: const pageChange = (e) => {
// Service.Request('/api/timing/detail', 'GET', { page.value = e
// date: date getRecordList()
// }).then(res => {
// // 处理返回的数据并更新表格
// tableData.value = res.data
// })
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// 页面背景色设置
page { page {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
// 页面容器 .baogan-container {
.timing-container {
min-height: 100vh; min-height: 100vh;
padding-bottom: 40rpx; padding-bottom: 40rpx;
} }
/* ==================== 页面标题区域 ==================== */
.header-section { .header-section {
background-color: #fff;
padding: 32rpx 28rpx 24rpx; padding: 32rpx 28rpx 24rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
@@ -329,29 +203,304 @@
} }
} }
/* ==================== 日历组件包装 ==================== */ .date-filter {
.calendar-wrapper { display: flex;
gap: 24rpx;
.date-picker {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
background-color: #fff; background-color: #fff;
margin-bottom: 20rpx; border-radius: 16rpx;
padding: 20rpx 0; padding: 20rpx;
.date-text {
.label {
display: block;
font-size: 26rpx;
color: #999;
}
.value {
display: block;
font-size: 30rpx;
color: #333;
margin-top: 4rpx;
font-weight: 600;
}
}
}
}
.select-section {
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
.select-label {
margin-bottom: 16rpx;
.label-text {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
}
.picker-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
transition: all 0.3s ease;
&:active {
background-color: #f0f0f0;
border-color: #faad14;
transform: scale(0.98);
}
.picker-text {
font-size: 28rpx;
color: #333;
}
}
}
.student-picker-modal {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 70vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx 32rpx 0 0;
z-index: 999;
animation: slideUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 -8rpx 40rpx rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
overflow: hidden;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 36rpx 32rpx 28rpx;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
flex-shrink: 0;
.header-title {
font-size: 36rpx;
font-weight: 700;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
.action-btn {
font-size: 26rpx;
color: #666;
padding: 14rpx 28rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
&.primary {
color: #fff;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%);
box-shadow: 0 4rpx 16rpx rgba(250, 173, 20, 0.3);
}
&:active {
transform: scale(0.92);
}
}
}
}
.student-list {
flex: 1;
overflow-y: auto;
padding: 0 20rpx 40rpx;
.student-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
border-radius: 20rpx;
margin-top: 16rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
position: relative;
overflow: hidden;
&:active {
transform: scale(0.98);
}
.item-checkbox {
flex-shrink: 0;
position: relative;
z-index: 1;
.checkbox-inner {
width: 40rpx;
height: 40rpx;
border-radius: 10rpx;
border: 2rpx solid #d9d9d9;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.checked {
border-color: #faad14;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
box-shadow: 0 2rpx 8rpx rgba(250, 173, 20, 0.3);
}
}
}
.item-avatar {
flex-shrink: 0;
position: relative;
z-index: 1;
.avatar-circle {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
&.male {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
}
&.female {
background: linear-gradient(135deg, #fa8c16 0%, #ffa940 50%, #d46b08 100%);
}
&::before {
content: '';
position: absolute;
top: 8rpx;
right: 8rpx;
width: 12rpx;
height: 12rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.avatar-text {
font-size: 34rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
}
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10rpx;
position: relative;
z-index: 1;
.item-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
.item-meta {
display: flex;
align-items: center;
gap: 12rpx;
.gender-badge {
font-size: 22rpx;
padding: 6rpx 14rpx;
border-radius: 10rpx;
font-weight: 500;
&.male {
color: #1890ff;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(64, 169, 255, 0.05) 100%);
}
&.female {
color: #fa8c16;
background: linear-gradient(135deg, rgba(250, 140, 22, 0.1) 0%, rgba(255, 169, 64, 0.05) 100%);
}
}
.age-text {
font-size: 22rpx;
color: #999;
}
}
}
}
}
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%);
backdrop-filter: blur(10rpx);
z-index: 998;
animation: fadeIn 0.3s ease;
} }
/* ==================== 日期数据统计卡片 ==================== */
.date-summary { .date-summary {
background-color: #fff; background-color: #fff;
margin: 0 20rpx 20rpx; margin: 0 20rpx 20rpx;
border-radius: 16rpx; border-radius: 20rpx;
padding: 24rpx; padding: 24rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
// 统计项
.summary-item { .summary-item {
text-align: center; text-align: center;
// 标签文字
.summary-label { .summary-label {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #999;
@@ -359,15 +508,13 @@
margin-bottom: 8rpx; margin-bottom: 8rpx;
} }
// 数值文字
.summary-value { .summary-value {
font-size: 32rpx; font-size: 32rpx;
font-weight: 700; font-weight: 700;
color: #1890ff; color: #faad14;
} }
} }
// 分隔线
.summary-divider { .summary-divider {
width: 1rpx; width: 1rpx;
height: 50rpx; height: 50rpx;
@@ -375,16 +522,14 @@
} }
} }
/* ==================== 表格区域 ==================== */
.table-section { .table-section {
margin: 0 20rpx; margin: 20rpx 20rpx;
background-color: #fff; background-color: #fff;
border-radius: 16rpx; border-radius: 20rpx;
overflow: hidden; overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
} }
/* ==================== 空状态容器 ==================== */
.empty-container { .empty-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -392,72 +537,79 @@
justify-content: center; justify-content: center;
padding: 80rpx 40rpx; padding: 80rpx 40rpx;
// 空状态图标
.empty-icon { .empty-icon {
margin-bottom: 24rpx; margin-bottom: 24rpx;
} }
// 空状态文字
.empty-text { .empty-text {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
} }
} }
/* ==================== 提示状态容器 ==================== */
// 用于在用户未选择日期时显示提示
.hint-state { .hint-state {
margin: 0 20rpx; margin: 0 20rpx;
background-color: #fff; background-color: #fff;
border-radius: 16rpx; border-radius: 20rpx;
padding: 80rpx 40rpx; padding: 80rpx 40rpx;
text-align: center; text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
// 提示图标
.hint-icon { .hint-icon {
margin-bottom: 24rpx; margin-bottom: 24rpx;
} }
// 提示文字
.hint-text { .hint-text {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
} }
} }
/* ==================== sl-table 样式覆盖 ==================== */
// 表格容器
::v-deep .sl-table { ::v-deep .sl-table {
width: 100%; width: 100%;
} }
// 表头背景色 - 使用蓝色渐变(计时功能的主色调)
::v-deep .sl-table__header { ::v-deep .sl-table__header {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%) !important; background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%) !important;
} }
// 表头单元格样式
::v-deep .sl-table__header__cell { ::v-deep .sl-table__header__cell {
color: #fff !important; color: #fff !important;
font-weight: 700; font-weight: 700;
font-size: 28rpx; font-size: 28rpx;
} }
// 表格单元格样式
::v-deep .sl-table__body__cell { ::v-deep .sl-table__body__cell {
font-size: 26rpx; font-size: 26rpx;
color: #333; color: #333;
padding: 24rpx 16rpx; padding: 24rpx 16rpx;
} }
// 偶数行背景色
::v-deep .sl-table__body__row:nth-child(even) { ::v-deep .sl-table__body__row:nth-child(even) {
background-color: #fafafa !important; background-color: #fafafa !important;
} }
// 奇数行背景色
::v-deep .sl-table__body__row:nth-child(odd) { ::v-deep .sl-table__body__row:nth-child(odd) {
background-color: #fff !important; background-color: #fff !important;
} }
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
</style> </style>

View File

@@ -97,7 +97,7 @@
<!-- 项目列表 --> <!-- 项目列表 -->
<view class="project-list"> <view class="project-list">
<text class="list-title" v-if="projects.length > 0">项目列表</text> <text class="list-title" v-if="projects.length > 0">项目列表</text>
<view v-if="projects.length > 0" class="list-container"> <scroll-view v-if="projects.length > 0" scroll-y="true" @scrolltolower='getList()' class="list-container">
<view v-for="project in projects" :key="project.planId" class="project-item" <view v-for="project in projects" :key="project.planId" class="project-item"
@click="handleProjectClick(project)"> @click="handleProjectClick(project)">
<view class="item-icon"> <view class="item-icon">
@@ -110,7 +110,7 @@
<text v-if="currentIndex!==3" class="item-count">{{ project.users.length }}</text> <text v-if="currentIndex!==3" class="item-count">{{ project.users.length }}</text>
</view> </view>
</view> </view>
</view> </scroll-view>
<view v-else class="empty-project"> <view v-else class="empty-project">
<text class="empty-text">暂无项目</text> <text class="empty-text">暂无项目</text>
</view> </view>
@@ -125,7 +125,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { onLoad } from '@dcloudio/uni-app' import { onLoad, onShow } from '@dcloudio/uni-app'
import { loginService } from '@/Service/swimming/loginService' import { loginService } from '@/Service/swimming/loginService'
import { PlanService } from '@/Service/swimming/PlanService' import { PlanService } from '@/Service/swimming/PlanService'
@@ -149,6 +149,10 @@
} }
}) })
onShow(()=>{
uni.showTabBar()
})
const login = () => { const login = () => {
uni.getProvider({ uni.getProvider({
service: 'oauth', service: 'oauth',
@@ -185,6 +189,7 @@
// 获取项目列表 // 获取项目列表
const getList = () => { const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') { if (status.value == 'loading' || status.value == 'nomore') {
return return
} }
@@ -652,6 +657,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16rpx; gap: 16rpx;
max-height: 240rpx;
} }
.project-item { .project-item {
@@ -660,7 +666,7 @@
gap: 20rpx; gap: 20rpx;
padding: 28rpx 24rpx; padding: 28rpx 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%); background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
border-radius: 20rpx; // border-radius: 20rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
position: relative; position: relative;

File diff suppressed because it is too large Load Diff

View File

@@ -351,6 +351,13 @@
}) })
Service.Msg(`${student.name}${student.segments.length}段已记录`) Service.Msg(`${student.name}${student.segments.length}段已记录`)
// 所有学生分段记录完成后自动停止计时
const allCompleted = students.value.every(s => s.segments.length >= maxSeg)
if (allCompleted) {
stopTimer()
Service.Msg('所有学生记录完成')
}
} }
// 重置学生 // 重置学生
@@ -382,13 +389,41 @@
return return
} }
console.log(students.value);
// 这里可以添加保存到后端或本地存储的逻辑 // 这里可以添加保存到后端或本地存储的逻辑
Service.Msg('保存成功', 'success') Service.Msg('保存成功', 'success')
// 保存后可以选择返回上一页 let data = [
setTimeout(() => { {
Service.GoPageBack() studentId: "",
}, 1000) studentName: "",
data: [{ circle: 0, time: "" }]
}
]
data = []
students.value.map((item : any) => {
let record = item.segments.map((content : any, index : any) => {
return {
circle: (index + 1)*segmentDistance.value,
time: content.time
}
})
data.push({
studentId: item.id,
studentName: item.name,
data: record
})
})
PlanService.AddPlanLog(planId.value,'分段项目','',JSON.stringify(data),'').then(res=>{
if(res.code==0){
Service.Msg('提交成功!')
}else{
Service.Msg(res.msg)
}
})
} }
// 显示学生记录弹窗 // 显示学生记录弹窗

View File

@@ -41,7 +41,8 @@
<text class="athlete-name">{{ athlete.name }}</text> <text class="athlete-name">{{ athlete.name }}</text>
</view> </view>
<view class="time-wrapper"> <view class="time-wrapper">
<text class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text> <text
class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text>
<text class="best-time">最快: <text class="best-time">最快:
{{ athlete.bestTime !== null ? formatTime(athlete.quicklyTime) : '无' }}</text> {{ athlete.bestTime !== null ? formatTime(athlete.quicklyTime) : '无' }}</text>
</view> </view>
@@ -58,6 +59,34 @@
</view> </view>
<view class="" style="width: 100%; height: 100rpx;"></view> <view class="" style="width: 100%; height: 100rpx;"></view>
<!-- 右下角悬浮提交按钮 -->
<view class="fab-submit" @click="submitData">
<text class="fab-text">提交</text>
</view>
<!-- 记录提示弹窗 -->
<u-popup :show="showRecord" mode="center" :round="20" :closeable="true" @close="showRecord=false" closeOnClickOverlay>
<view class="record-notice-modal">
<view class="notice-header">
<text class="notice-title">恭喜以下学生打破记录</text>
</view>
<view class="notice-table">
<view class="table-header">
<text class="header-cell index-cell">序号</text>
<text class="header-cell name-cell">学生姓名</text>
<text class="header-cell time-cell">记录时间</text>
</view>
<scroll-view scroll-y class="table-body">
<view v-for="(item,index) in record" class="table-row">
<text class="row-cell index-cell">{{ index+1 }}</text>
<text class="row-cell name-cell">{{ item.name }}</text>
<text class="row-cell time-cell">{{ formatTime(item.quicklyTime) }}</text>
</view>
</scroll-view>
</view>
</view>
</u-popup>
<!-- 底部控制按钮 --> <!-- 底部控制按钮 -->
<view class="control-buttons"> <view class="control-buttons">
<view class="button-group"> <view class="button-group">
@@ -81,7 +110,10 @@
<text class="btn-text">暂停</text> <text class="btn-text">暂停</text>
</button> </button>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
@@ -123,6 +155,8 @@
const groupSize = ref('0') const groupSize = ref('0')
// 计时器状态 // 计时器状态
const isRunning = ref(false) const isRunning = ref(false)
const currentTime = ref(0) const currentTime = ref(0)
@@ -136,7 +170,8 @@
const stopwatchMode = ref<'interval' | 'together'>('interval') const stopwatchMode = ref<'interval' | 'together'>('interval')
// 间隔时间(秒) // 间隔时间(秒)
const intervalTime = ref('0') const intervalTime = ref('0')
let record=ref<Array<any>>([])
let showRecord=ref(false)
onLoad((data : any) => { onLoad((data : any) => {
planId.value = data.id planId.value = data.id
@@ -349,6 +384,52 @@
}) })
}) })
} }
// 处理数据
const formatMinuteTime = (seconds : number) : string => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
const ms = Math.floor((seconds % 1) * 1000)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`
}
// 提交数据
const submitData = () => {
stopTimer()
const hasData = athletes.value.some(a => a.time > 0)
if (!hasData) {
Service.Msg('暂无数据可提交')
return
}
let data = [{
planName: "",
studentId: "",
studentName: "",
time: ""
}]
data = []
athletes.value.map((item : any) => {
data.push({
planName: planName.value,
studentId: item.studentId,
studentName: item.name,
time: formatMinuteTime(item.time)
})
})
PlanService.AddPlanLog(planId.value,'计时项目',JSON.stringify(data),'','','').then(res=>{
if(res.code==0){
Service.Msg('提交成功!')
if(res.data.record.length>0){
record.value=res.data.record
showRecord.value=true
}
}else{
Service.Msg(res.msg)
}
})
}
// 页面卸载时清理计时器 // 页面卸载时清理计时器
@@ -403,7 +484,7 @@
/* 选手列表区域 */ /* 选手列表区域 */
.athletes-section { .athletes-section {
margin-top: 20rpx; margin-top: 80rpx;
margin-bottom: 120rpx; margin-bottom: 120rpx;
.section-header { .section-header {
@@ -586,6 +667,34 @@
} }
} }
/* 右下角悬浮提交按钮 */
.fab-submit {
position: fixed;
right: 30rpx;
bottom: 180rpx;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: linear-gradient(135deg, #fa8c16 0%, #ffa940 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 8rpx 24rpx rgba(250, 140, 22, 0.5),
0 16rpx 40rpx rgba(0, 0, 0, 0.2);
z-index: 200;
&:active {
transform: scale(0.92);
}
.fab-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 600;
}
}
/* 底部控制按钮 */ /* 底部控制按钮 */
.control-buttons { .control-buttons {
display: flex; display: flex;
@@ -597,7 +706,7 @@
right: 0; right: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%); background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%);
padding: 32rpx 40rpx; padding: 32rpx 40rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); // padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid rgba(0, 0, 0, 0.06); border-top: 1rpx solid rgba(0, 0, 0, 0.06);
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08); box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08);
@@ -611,7 +720,8 @@
.start-btn, .start-btn,
.pause-btn, .pause-btn,
.reset-all-btn, .reset-all-btn,
.record-btn { .record-btn,
.submit-btn {
flex: 1; flex: 1;
height: 100rpx; height: 100rpx;
border-radius: 20rpx; border-radius: 20rpx;
@@ -719,4 +829,84 @@
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
} }
} }
/* 记录提示弹窗 */
.record-notice-modal {
width: 560rpx;
padding: 30rpx 40rpx 48rpx;
background-color: #ffffff;
border-radius: 20rpx;
text-align: center;
.notice-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
.notice-title {
font-size: 36rpx;
font-weight: 600;
color: #333333;
margin-top: 16rpx;
}
}
.notice-table {
width: 100%;
.table-header {
display: flex;
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 20rpx 0;
margin-bottom: 16rpx;
.header-cell {
font-size: 26rpx;
font-weight: 600;
color: #666666;
text-align: center;
}
}
.table-body {
max-height: 360rpx;
.table-row {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.row-cell {
font-size: 28rpx;
color: #333333;
text-align: center;
}
.time-cell {
color: #ff0000;
font-family: 'DIN Alternate', monospace;
font-weight: 600;
}
}
}
.index-cell {
width: 80rpx;
}
.name-cell {
flex: 1;
}
.time-cell {
width: 160rpx;
}
}
}
</style> </style>

View File

@@ -0,0 +1,48 @@
## 1.2.32024-10-17
更新demo
## 1.2.22024-09-26
更新说明
## 1.2.12024-09-25
更新说明文档
## 1.2.02024-09-14
更新说明
## 1.1.92024-09-12
app demo安装包
## 1.1.82024-08-19
更新demo
## 1.1.72024-08-19
更新说明
## 1.1.62024-07-31
增加超集功能演示demo
## 1.1.52024-07-25
修复width配置bug
## 1.1.42024-04-08
增加禁用功能
## 1.1.32024-01-31
修复align配置不生效问题
## 1.1.22024-01-26
更新vue2使用说明
## 1.1.12024-01-25
修复vue2版本使用兼容问题
## 1.1.02023-12-21
修复vue2编译报错问题
## 1.0.92023-11-17
修复图片fixed时无法固定的bug
## 1.0.82023-11-17
修复主题样式bug
## 1.0.72023-09-21
修复说明
## 1.0.62023-07-20
更新使用要求说明
## 1.0.52023-07-20
更新primaryColor的支持说明
## 1.0.42023-07-14
更新package.json文件
## 1.0.32023-07-14
增加表格row激活主题颜色配置
## 1.0.22023-07-13
增加动态颜色环境变量
## 1.0.12023-07-13
增加主体颜色配置
## 1.0.02023-07-13
初始化next-table

View File

@@ -0,0 +1,187 @@
<template>
<!-- #ifdef VUE3 -->
<view class="uni-table-checkbox" @click.stop="selected" :style="`--primaryColor: ${primaryColor}`">
<!-- #endif -->
<!-- #ifdef VUE2 -->
<view class="uni-table-checkbox" @click.stop="selected" :style="{'--primaryColor': primaryColor}">
<!-- #endif -->
<view v-if="!indeterminate" class="checkbox__inner" :class="{'is-checked':isChecked,'is-disable':isDisabled}">
<view class="checkbox__inner-icon"></view>
</view>
<view v-else class="checkbox__inner checkbox--indeterminate">
<view class="checkbox__inner-icon"></view>
</view>
</view>
</template>
<script>
export default {
name: 'TableCheckbox',
emits:['checkboxSelected'],
props: {
indeterminate: {
type: Boolean,
default: false
},
checked: {
type: [Boolean,String],
default: false
},
disabled: {
type: Boolean,
default: false
},
index: {
type: Number,
default: -1
},
cellData: {
type: Object,
default () {
return {}
}
},
primaryColor: {
type: String,
default: '#f0ad4e'
}
},
watch:{
checked(newVal){
if(typeof this.checked === 'boolean'){
this.isChecked = newVal
}else{
this.isChecked = true
}
},
indeterminate(newVal){
this.isIndeterminate = newVal
}
},
data() {
return {
isChecked: false,
isDisabled: false,
isIndeterminate:false,
}
},
created() {
if(typeof this.checked === 'boolean'){
this.isChecked = this.checked
}
this.isDisabled = this.disabled
},
methods: {
selected() {
if (this.isDisabled) return
this.isIndeterminate = false
this.isChecked = !this.isChecked
this.$emit('checkboxSelected', {
checked: this.isChecked,
data: this.cellData
})
}
}
}
</script>
<style lang="scss" scoped>
$border-color: #DCDFE6;
$disable:0.4;
.uni-table-checkbox {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: relative;
margin: 5px 0;
cursor: pointer;
// 多选样式
.checkbox__inner {
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 2px;
background-color: #fff;
z-index: 1;
.checkbox__inner-icon {
position: absolute;
/* #ifdef APP-NVUE */
top: 2px;
/* #endif */
/* #ifndef APP-NVUE */
top: 2px;
/* #endif */
left: 5px;
height: 7px;
width: 3px;
border: 1px solid #fff;
border-left: 0;
border-top: 0;
opacity: 0;
transform-origin: center;
transform: rotate(45deg);
box-sizing: content-box;
}
&.checkbox--indeterminate {
border-color: var(--primaryColor);
background-color: var(--primaryColor);
.checkbox__inner-icon {
position: absolute;
opacity: 1;
transform: rotate(0deg);
height: 2px;
top: 0;
bottom: 0;
margin: auto;
left: 0px;
right: 0px;
bottom: 0;
width: auto;
border: none;
border-radius: 2px;
transform: scale(0.5);
background-color: #fff;
}
}
&:hover{
border-color: var(--primaryColor);
}
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
background-color: #F2F6FC;
border-color: $border-color;
}
// 选中
&.is-checked {
border-color: var(--primaryColor);
background-color: var(--primaryColor);
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
// 选中禁用
&.is-disable {
opacity: $disable;
}
}
}
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div class="table-empty">
<text>{{text}}</text>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
default: '暂无数据'
}
}
}
</script>
<style lang="scss" scoped>
.table-empty {
width: 100%;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 1px solid #e8e8e8;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<view class="table-h5-footer top-header-uni" :style="{paddingRight:`${scrollbarSize}px`}">
<scroll-view
class="next-table-headers"
@scroll="handleFooterTableScrollLeft"
scroll-x="true"
scroll-y="false"
id="tableFooterHeaders"
scroll-anchoring="true"
:scroll-left="headerFooterTableLeft"
style="padding-bottom: 0px;
background: #fafafa;height: 100%">
<view class="next-table-fixed" >
<view class="next-table-thead" style="position: relative;" >
<view class="item-tr">
<view
class="item-th"
:style="{
width:`${item.width?item.width:'100'}px`,
flex:index===transColumns.length-1?1:'none',
minWidth:`${item.width?item.width:'100'}px`,
borderRight:`${border?'1px solid #e8e8e8':''}`,
borderTop:`${border?'1px solid #e8e8e8':''}`,
textAlign:item.align||'left'
}"
v-for="(item,index) in transColumns" :key="index">
{{ sums[index] }}
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import summary from '../js/summary.js'
export default {
name:'table-footer',
mixins:[summary],
}
</script>
<style lang="scss" scoped>
.table-h5-footer {
background: #fafafa;
/*每个页面公共css */
scroll-view ::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
//第二种
::-webkit-scrollbar{
display: none;
}
.item-tr{
display: flex;
}
.item-th{
padding-left: 8px;
line-height: 39px;
height: 40px;
box-sizing: border-box;
flex-shrink: 0;
width: 100px;
padding-right: 20px;
word-break: keep-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
border-bottom: 1px solid #e8e8e8;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template >
<view class="table-load-more">
<image :src="base64Flower" style="" class="loading-custom-image"></image>
<text>正在加载中...</text>
</view>
</template>
<script>
const base64Flower = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkBAMAAACCzIhnAAAAKlBMVEVHcEzDw8Ovr6+pqamUlJTCwsKenp61tbWxsbGysrLNzc2bm5u5ubmjo6MpovhuAAAACnRSTlMA/P79/sHDhiZS0DxZowAABBBJREFUWMPtl89rE0EUx7ctTXatB3MI1SWnDbUKPUgXqh4ED8Uf7KUVSm3ooVSpSii0Fn/gD4j4o+APiEoVmos9FO2celiqZVgwgaKHPQiCCkv+F99kM7Ozm5kxq1dfD91k9pPve9/3ZjbRNHHok/mKli4eIPNgSuRObuN9SqSEzM20iGnm0yIbqCuV7NSSSIV7uyPM6JMBYdeTOanh/QihJYZsUCSby+VkMj2AvOt0rAeQAwqE3lfKMZVlQCZk1QOCKkkVPadITCfIRNKxfoJI5+0OIFtJx14CMSg1mRSDko7VAfksRQzEbGYqxOJcVTWMCH2I1/IACNW0PWU2M8cmAVHtnH5mM1VRWtwKZjOd5JbF6s1IbaYqaotjNlPHgDAnlAizubTR6ovMYn052g/U5qcmOpi0WL8xTS/3IfSet5m8MEr5ajjF5le6dq/OJpobrdY0t3i9QgefWrxW9/1BLhk0E9m8FeUMhhXal499iD0eQRfDF+ts/tttORRerfp+oV7f4xJj82iUYm1Yzod+ZQEAlS/8mMBwKebVmCVp1f0JLS6zKd17+iwRKTARVg2SHtz3iEbBH+Q+U28zW2Jiza8Tjb1YFoYZMsJyjDqp3M9XBQdSdPLFdxEpvOB37JrHcmR/y9+LgoTlCFGZEa2sc6d4PGlweEa2JSVPoVm+IfGG3ZL037iV9oH+P+Jxc4HGVflNq1M0pivao/EopO4b/ojVCP9GjmiXOeS0DOn1o/iiccT4ORnyvBGF3yUywkQajW4Ti0SGuiy/wVSg/L8w+X/8Q+hvUx8Xd90z4oV5a1i88MbFWHz0WZZ1UrTwBGPX3Rat9AFiXRMRjoMdIdJLEOt2h7jrYOzgOamKZSWSNspOS0X8SAqRYmxRL7sg4eLzYmNehcxh3uoyud/BH2Udux4ywxFTc1xC7Mgf4vMhc5S+kSH3Y7yj+qpwIWSoPTVCOOPVthGx9FbGqrwFw6wSFxJr+17zeKcztt3u+2roAEVgUjDd+AHGuxHy2rZHaa8JMkTHEeyi85ANPO9j9BVuBRD2FY5LDMo/Sz/2hReqGIs/KiFin+CsPsYO/yvM3jL2vE8EbX7/Bf8ejtr2GLN65bioAdgLd8Bis/mD5GmP2qeqyo2ZwQEOtAjRIDH7mBKpUcMoApbZJ5UIxkEwxyMZyMxW/uKFvHCFR3SSmerHyDNQ2dF4JG6zIMpBgLfjSF9x1D6smFcYnGApjmSLICO3ecCDWrQ48geba9DI3STy2i7ax6WIB62fSyIZIiO3GFQqSURp8wCo7GhJBGwuSovJBNjb7kT6FPVnIa9qJ2Ko+l9mefGIdinaMp0yC1URYiwsdfNE45EuA5Cx9EhalfvN5s+UyItm81vaB3p4joniN+SCP7Qc1hblAAAAAElFTkSuQmCC';
export default {
data(){
return{
base64Flower
}
}
}
</script>
<style lang="scss" scoped>
.table-load-more {
width: 100%;
position: absolute;
bottom: 0;
left: 0;
z-index: 999;
background: white;
display: flex;
height: 40px;
flex-shrink: 0;
align-items: center;
justify-content: center;
.loading-custom-image{
color: #a4a4a4;
margin-right: 8rpx;
width: 24px;
height: 24px;
/* #ifndef APP-NVUE */
animation: loading-circle 1s linear infinite;
/* #endif */
}
@keyframes loading-circle {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<!-- #ifdef VUE3 -->
<view class="table-paging" :style="`--primaryColor: ${primaryColor}`">
<!-- #endif -->
<!-- #ifdef VUE2 -->
<view class="table-paging" :style="{'--primaryColor': primaryColor}">
<!-- #endif -->
<view
:class="{
'table-paging-item': true,
turner: true,
disabled: pageIndex <= 1,
}"
@click="prePage"
>
<text>上一页</text>
</view>
<view class="table-paging-item page">
<text class="table-paging-current">{{ pageIndex || "1" }}</text>
<text class="table-paging-gap">/</text>
<text>{{ pageTotal || "0" }}</text>
</view>
<view
:class="{
'table-paging-item': true,
turner: true,
disabled: pageIndex >= pageTotal,
}"
@click="nextPage"
>
<text>下一页</text>
</view>
</view>
</template>
<script>
export default {
emits:['change'],
props: {
pageIndex: {
type: Number,
default: 1
},
pageTotal: {
type: Number,
default: 0
},
primaryColor: {
type: String,
default: '#f0ad4e'
}
},
methods: {
// 上一页
prePage() {
if (this.pageIndex <= 1) {
return;
}
this.$emit("change", this.pageIndex - 1);
},
// 下一页
nextPage() {
if (this.pageIndex >= this.pageTotal) {
return;
}
this.$emit("change", this.pageIndex + 1);
}
}
}
</script>
<style lang="scss" scoped>
$primary-color: var(--primaryColor);
.table-paging {
height: 40px;
display: flex;
align-items: stretch;
justify-content: space-between;
font-size: 14px;
color: $primary-color;
line-height: 1;
}
.table-paging-item {
display: flex;
align-items: center;
&.page {
color: #999999ff;
}
&.turner {
padding: 0 5px;
transition: all 0.2s linear;
&:hover {
opacity: 0.8;
}
}
&.disabled {
cursor: not-allowed;
color: #999999;
}
}
.table-paging-current {
color: $primary-color;
font-size: 14px;
font-weight: 500;
}
.table-paging-gap {
margin: 0 5px;
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<view class="next-table-header" style="display: flex;" >
<view class="item-tr" >
<view class='item-td'
:style="{
width:`${item.width?item.width:'100'}px`,
borderRight:`${border?'1px solid #e8e8e8':''}`,
textAlign:item.align||'left'
}"
:key="`15255966555${index}`"
v-for="(item,index) in fixedLeftColumns">
<template >
{{sums[index]}}
</template>
</view>
</view>
</view>
</template>
<script>
import summary from '../js/summary.js'
export default {
mixins:[summary]
}
</script>
<style lang="scss" scoped>
.next-table-header {
overflow: hidden;
background: #fafafa;
.item-th{
padding-left: 8px;
line-height: 39px;
height: 40px;
//display: flex;
//align-items: center;
box-sizing: border-box;
}
}
.item-tr{
display: flex;
box-sizing: border-box;
}
.item-td{
flex-shrink: 0;
width: 100px;
padding-left: 8px;
height: 40px;
line-height: 40px;
padding-right: 20px;
box-sizing: border-box;
word-break: keep-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
border-bottom: 1px solid #e8e8e8;
background: rgb(250, 250, 250);
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<view class="next-table-footer" style="height: 40px;">
<view class="next-table-fixed" >
<view class="next-table-thead" style="position: relative;" >
<view class="item-tr">
<view
:class="['item-th',index <fixedLeftColumns.length&&'zb-stick-side']"
:style="{
left:`${item.left}px`,
width:`${item.width?item.width:'100'}px`,
flex:index===transColumns.length-1?1:'none',
minWidth:`${item.width?item.width:'100'}px`,
borderRight:`${border?'1px solid #e8e8e8':''}`,
borderTop:`${border?'1px solid #e8e8e8':''}`,
textAlign:item.align||'left'
}"
v-for="(item,index) in transColumns" :key="index">
<template>
{{ sums[index]||item.emptyString }}
</template>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import summary from '../js/summary.js'
export default {
mixins:[summary]
}
</script>
<style lang="scss" scoped>
.next-table-footer {
background: #fafafa;
width: fit-content;
min-width: 100%;
position: sticky;
bottom: 0;
z-index: 2;
.item-tr{
display: flex;
min-width: 100%;
}
.item-th{
padding-left: 8px;
line-height: 39px;
height: 40px;
//display: flex;
//align-items: center;
box-sizing: border-box;
flex-shrink: 0;
width: 100px;
padding-right: 20px;
word-break: keep-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
border-bottom: 1px solid #e8e8e8;
}
.next-table-fixed{
min-width: 100%;
}
.zb-stick-side{
position: sticky;
bottom:0 ;
left: 0;
z-index: 2;
//border-right: solid 1rpx #dbdbdb;
box-sizing: border-box;
background: #fafafa;
//box-shadow: 6px 0 6px -4px #ccc;
}
}
</style>

View File

@@ -0,0 +1,71 @@
export default {
props: {
highlight: {
type: Boolean,
default: false
},
itemDate: {
type: Object,
default: () => {}
},
columns: {
type: Array,
default: () => []
},
showSummary: {
type: Boolean,
default: false
},
isShowLoadMore: {
type: Boolean,
default: false
},
data: {
type: Array,
default: () => []
},
sumText: {
type: String,
default: '合计'
},
showHeader: {
type: Boolean,
default: true
},
border: {
type: Boolean,
default: false
},
stripe: {
type: Boolean,
default: true
},
fit: {
type: Boolean,
default: false
},
showPaging: {
type: Boolean,
default: false
},
pageIndex: {
type: Number,
default: 1
},
pageTotal: {
type: Number,
default: 0
},
primaryColor: {
type: String,
default: '#f0ad4e'
},
rowKey: [String, Function],
summaryMethod: Function,
pullUpLoading: Function,
formatter: Function,
cellStyle: Function,
cellHeaderStyle: Function,
permissionBtn: Function,
}
}

View File

@@ -0,0 +1,88 @@
export default {
props:{
scrollbarSize:{
type:Number,
default:0
},
fixedLeftColumns:{
type:Array,
default:()=>[]
},
data:{
type:Array,
default:()=>[]
},
transColumns:{
type:Array,
default:()=>[]
},
border:{
type:Boolean,
default:false
},
showSummary:{
type:Boolean,
default:false
},
summaryMethod:{
type:Function
},
sumText:{
type:String,
default:'合计'
},
headerFooterTableLeft:{
type:Number,
default:0
},
handleFooterTableScrollLeft:Function,
},
data(){
return{
sums:[]
}
},
watch:{
'data':{
deep:true,
immediate:true,
handler(newValue,oldValue){
let sums = [];
if (this.summaryMethod) {
sums = this.summaryMethod({ columns: this.transColumns, data: this.data });
} else {
this.transColumns.forEach((column, index) => {
if (index === 0) {
sums[index] = this.sumText;
return;
}
const values = this.data.map(item => Number(item[column.name]));
const precisions = [];
let notNumber = true;
values.forEach(value => {
if (!isNaN(value)) {
notNumber = false;
let decimal = ('' + value).split('.')[1];
precisions.push(decimal ? decimal.length : 0);
}
});
const precision = Math.max.apply(null, precisions);
if (!notNumber) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr);
if (!isNaN(value)) {
return parseFloat((prev + curr).toFixed(Math.min(precision, 20)));
} else {
return prev;
}
}, 0);
} else {
sums[index] = '';
}
});
}
this.sums = sums
},
}
}
}

View File

@@ -0,0 +1,91 @@
/**
* 获取滚动条宽度
*/
let cached = undefined;
export const getScrollbarSize = fresh => {
// #ifdef H5
if (fresh || cached === undefined) {
let inner = document.createElement("div");
let innerStyle = inner.style;
innerStyle.width = "100%";
innerStyle.height = "200px";
let outer = document.createElement("div");
let outerStyle = outer.style;
outerStyle.position = "absolute";
outerStyle.top = 0;
outerStyle.left = 0;
outerStyle.pointerEvents = "none";
outerStyle.width = "200px";
outerStyle.height = "150px";
outerStyle.visibility = "hidden";
outer.appendChild(inner);
document.body.appendChild(outer);
// 设置子元素超出部分隐藏
outerStyle.overflow = "hidden";
let width1 = inner.offsetWidth;
// 设置子元素超出部分滚动
outer.style.overflow = "scroll";
let width2 = inner.offsetWidth;
if (width1 === width2) {
width2 = outer.clientWidth;
}
document.body.removeChild(outer);
cached = width1 - width2;
}
//#endif
return cached;
};
// 16进制转换rgba
export const colorHextoRGB = (val, opacity = 0.7) => {
let color = val;
const t = {},
bits = (color.length == 4) ? 4 : 8,//假设是shorthand。 #fff, 那么bits为4位, 每一位代表的个属性, 其他的为8位 每两位代表一个属性 #ffffff00
mask = (1 << bits) - 1; //表示字节占位符。 向左移4位或8位var a = (1 << 4 ) - 1 -> 10000 - 1, a.toString(2); // 1111。或者 8位的 1111 1111
color = Number("0x" + color.substr(1)); //#ff0000 转变为16进制0xff0000;
if(isNaN(color)){
return null; // Color
}
["b", "g", "r"].forEach(function(x){
const c = color & mask;
color >>= bits;
t[x] = bits == 4 ? 17 * c : c; // 0xfff 一个f应该代表 255, 应该当[0-255]按15等份划分每一等份间隔 17。 所以获得的值须要乘以17, 才干表示rgb中255的值
});
const rgba='rgba('+ t.r + ',' +t.g + ','+ t.b +',' + opacity+')'
return rgba; // Color
}
// rgba转换16进制
export const hexify = (color) => {
const values = color
.replace(/rgba?\(/, '')
.replace(/\)/, '')
.replace(/[\s+]/g, '')
.split(',');
const a = parseFloat(values[3] || 1),
r = Math.floor(a * parseInt(values[0]) + (1 - a) * 255),
g = Math.floor(a * parseInt(values[1]) + (1 - a) * 255),
b = Math.floor(a * parseInt(values[2]) + (1 - a) * 255);
return "#" +
("0" + r.toString(16)).slice(-2) +
("0" + g.toString(16)).slice(-2) +
("0" + b.toString(16)).slice(-2);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
{
"id": "next-table",
"displayName": "next-table多功能表格多选checkbox、删除编辑、合计分页",
"version": "1.2.3",
"description": "表格组件 支持固定表头和首列、上拉加载更多、及固定多列表格自适应内容排序多选checkbox、可点击删除编辑、合计功能分页自定义主题兼容多端",
"keywords": [
"table",
"固定表头、固定列、多选checkbox"
],
"repository": "",
"engines": {
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "component-vue",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-",
"xhs": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,509 @@
## 介绍
基于uni-app开发的一个普通的表格组件功能有固定首列和表头、排序、操作按钮、
table 表格 固定表头、固定首列、多列 上拉加载更多、 排序、自适应列宽、多选checkbox、编辑、删除、按钮、合计 多页功能
已用于生产环境
> 遇到问题或有建议可以加入QQ群(<font color=#f00>455948571</font>)反馈
> 如果觉得组件不错,<font color=#f00>给五星鼓励鼓励</font>咯!
## 参考依赖
本组件居于github开源项目[zb-table](https://github.com/zouzhibin/zb-table#readme)进行二开,功能会有些差异和增强,如果有需要原版请参考源开源项目。感谢作者
## 注意本插件依赖于scss的编译如果没有使用scss请手动改源码去掉scss的语法方可使用。如果有疑问请加入加入QQ群(<font color=#f00>455948571</font>)
## 注意
### 作者不介意你对组件源码进行改造使用,为了开源更加高效,谢谢你的配合;为了节省不必要的沟通浪费,以下情况请不要再反馈给作者,请自行解决;
### 在这感各位的理解,我支持开源,但是作者时间有限;谢谢各位的配合;在这里期望我写的小小插件能为你提供便捷;
> 1.如果你对源码进行了修改使用,请不需要对作者做任何的反馈,作者确实没有空陪你做技术分析解答;
> 2.如果你引入插件连插件是否有正常被uniapp框架识别解析都不清楚请你换个插件使用
> 3.如果你引入插件,针对自己项目进行功能改造的,请自行仔细阅读源码并了解其原理,自行改造;这里作者不愿意浪费过多时间进行技术解答;
### 如果有使用问题请加群
注意如果插件问题请务必给一个完整的复现demo谢谢配合
[点击链接加入群聊前端开发uniapp插件](https://qm.qq.com/q/S1bJzQfJAG)
## 使用
>[从uniapp插件市场导入](https://ext.dcloud.net.cn/plugin?name=next-table)
### 微信小程序在线体验
![](https://lixueshiaa.github.io/webtest/www/static/img/ponder_next.png)
### 预览
### appDemo安装包下载地址[android安装包](https://lixueshiaa.github.io/webtest/www/static/demo_next.apk);
***
| 功能预览 |
| :----------------------------------------------------------: |
| ![](https://lixueshiaa.github.io/webtest/www/static/next-table.gif) |
### 超集功能预览(增值功能-请下载next-x-table支持可固定表头分组表头等强大的功能)
### 点击进入: [next-x-table](https://ext.dcloud.net.cn/plugin?id=19584)
###
| 小程序和app端随意设置容易宽高冻结表头使得交互更加友好 | 动态分组表头/分页/排序/合计/fixed等功能 |
| :--------------------------------------------------------------------: | :-----------------------------------------------------------------------: |
| ![](https://lixueshiaa.github.io/webtest/www/static/next-table-aa.gif) | ![](https://lixueshiaa.github.io/webtest/www/static/next-x-table-app.gif) |
| 分页功能演示 | 下拉加载更多等功能 |
| :-----------------------------------------------------------------------------: |:-----------------------------------------------------------------------: |
| ![](https://lixueshiaa.github.io/webtest/www/static/next-x-table-mapping-a.gif) |![](https://lixueshiaa.github.io/webtest/www/static/next-x-table-mapping.gif) |
## 示例demo(vue3 + ts)
``` html
<next-table
:show-header="true"
:columns="column"
:stripe="true"
:fit="false"
show-summary
sum-text="合计"
@rowClick="rowClick"
:summary-method="getSummaries"
@toggleRowSelection="toggleRowSelection"
@toggleAllSelection="toggleAllSelection"
:border="true"
@edit="buttonEdit"
@dele="dele"
:data="datalist"></next-table>
```
```js
<script setup lang="ts">
import {ref, unref} from "vue"
const pageIndex = ref(1)
const pageTotal = ref(5)
const datalist = ref([])
const checkNameList = ref([])
function getdatalist(pageIndex) {
const pageSize = 10
const arr = []
for(let i = pageSize*(pageIndex-1) + 1; i < pageSize*pageIndex; i++) {
arr.push({
date: '2023-06-23',
name: `刘先生${i}`,
province: '上海',
sex: i%2 ? '0' : '1',
disabled: i%2 ? true : false,
checked: unref(checkNameList)[unref(pageIndex)] ? unref(checkNameList)[unref(pageIndex)].indexOf(`刘先生${i}`) !== -1 : false,
age: 20,
img: 'https://gss0.baidu.com/-Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=e58ae9feb1003af34defd464001aea6a/8601a18b87d6277f4d763bcf2f381f30e824fce5.jpg',
city: '广州市',
address: '天河区东圃镇2002号',
zip: 200333
})
}
datalist.value = arr
}
function pageChange(index) {
pageIndex.value = index
getdatalist(unref(pageIndex))
}
function dele(item) {
const index = unref(datalist).findIndex(it => it.name == item.name)
if (index != -1) {
unref(datalist).splice(index, 1)
}
}
function toggleAllSelection(_, list) {
unref(checkNameList)[unref(pageIndex)] = list.map(item => item.name)
}
function toggleRowSelection(bool, list) {
unref(checkNameList)[unref(pageIndex)] = list.map(item => item.name)
}
function buttonEdit(item) {
console.log(111111, item)
}
const column = ref([
{ type:'selection', fixed:true,width:60 },
{ name: 'name', label: '姓名',fixed:false,width:80,emptyString:'--' },
{ name: 'age', label: '年纪',sorter:false,align:'right', },
{ name: 'sex', label: '性别',filters:{'0':'男','1':'女'}},
{ name: 'img', label: '图片',type:"img" },
{ name: 'address', label: '地址' },
{ name: 'date', label: '日期',sorter:true },
{ name: 'province', label: '省份' },
{ name: 'city', label: '城市' },
{ name: 'zip', label: '邮编' },
{ name: 'operation', type:'operation',label: '操作',renders:[
{
name:'编辑',
func:'edit' // func 代表子元素点击的事件 父元素接收的事件 父元素 @edit
},
{
name:'删除',
type:'warn',
func:"dele"
},
]},
])
getdatalist(unref(pageIndex))
</script>
```
## 多级表头示例demo同样支持vue2(vue3 + ts)
``` html
<template>
<view class="next-table-container">
<next-x-table
:show-header="true"
:columns="column"
:stripe="true"
:fit="true"
:show-summary="true"
sum-text="合计"
@rowClick="rowClick"
:summary-method="getSummaries"
@pageChange="pageChange"
@toggleRowSelection="toggleRowSelection"
@toggleAllSelection="toggleAllSelection"
:pageIndex="pageIndex"
:pageTotal="pageTotal"
align="center"
:showPaging="true"
:border="true"
:isShowLoadMore="false"
:highlight="true"
@edit="buttonEdit"
@dele="dele"
:pullUpLoading="pullUpLoading"
:data="datalist" />
</view>
</template>
<script setup lang="ts">
import {ref, unref, onMounted, nextTick } from "vue"
const pageIndex = ref(1)
const pageTotal = ref(5)
const datalist = ref([])
const checkNameList = ref([])
const tableWidth = ref(0)
const tableHeight = ref(0)
const pageSize = 30
function getdatalist(pageIndex) {
const arr = []
for(let i = pageSize*(pageIndex-1) + 1; i < pageSize*pageIndex; i++) {
arr.push({
date: '2023-06-23',
name: `刘先生${i}`,
province: '上海',
sex: i%2 ? '0' : '1',
other: "其他类型",
remark: `备注-${i}`,
disabled: i%2 ? true : false,
checked: unref(checkNameList)[unref(pageIndex)] ? unref(checkNameList)[unref(pageIndex)].indexOf(`刘先生${i}`) !== -1 : false,
age: 20,
img: 'https://gss0.baidu.com/-Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=e58ae9feb1003af34defd464001aea6a/8601a18b87d6277f4d763bcf2f381f30e824fce5.jpg',
city: '广州市',
address: '天河区东圃镇2002号发撒锻炼腹肌爱的色放上帝发誓地方',
zip: 200333
})
}
if(pageIndex === 1) {
datalist.value = arr
} else {
datalist.value = datalist.value.concat(arr)
}
}
function rowClick(e) {
console.log('------rowClick------', e)
}
function _getdatalist(pageIndex) {
const arr = []
for(let i = pageSize*(pageIndex-1) + 1; i < pageSize*pageIndex; i++) {
arr.push({
date: '2023-06-23',
name: `刘先生${i}`,
province: '上海',
sex: i%2 ? '0' : '1',
other: "其他类型",
remark: `备注-${i}`,
disabled: i%2 ? true : false,
checked: unref(checkNameList)[unref(pageIndex)] ? unref(checkNameList)[unref(pageIndex)].indexOf(`刘先生${i}`) !== -1 : false,
age: 20,
img: 'https://gss0.baidu.com/-Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=e58ae9feb1003af34defd464001aea6a/8601a18b87d6277f4d763bcf2f381f30e824fce5.jpg',
city: '广州市',
address: '天河区东圃镇2002号发撒锻炼腹肌爱的色放上帝发誓地方',
zip: 200333
})
}
datalist.value = arr
}
function settingConf(type) {
if(type === 1) {
tableWidth.value = "50vw";
tableHeight.value = "50vh";
} else if(type === 2) {
tableWidth.value = "80vw";
tableHeight.value = "70vh";
} else if(type === 3) {
tableWidth.value = "100vw";
tableHeight.value = "80vh";
}
}
function pageChange(index) {
console.log(1111111, index)
pageIndex.value = index
_getdatalist(unref(pageIndex))
}
function dele(item) {
const index = unref(datalist).findIndex(it => it.name == item.name)
if (index != -1) {
unref(datalist).splice(index, 1)
}
}
function toggleAllSelection(_, list) {
unref(checkNameList)[unref(pageIndex)] = list.map(item => item.name)
}
function toggleRowSelection(bool, list) {
unref(checkNameList)[unref(pageIndex)] = list.map(item => item.name)
}
function buttonEdit(item) {
console.log(111111, item)
}
function pullUpLoading(callback) {
console.log(1111111111111, pageIndex.value)
pageIndex.value = pageIndex.value + 1
setTimeout(() => {
if(pageIndex.value > 3) {
callback('ok')
} else {
getdatalist(unref(pageIndex))
callback()
}
}, 2000)
}
const column = ref([
{ type:'selection',width:60, fixed: true },
{ name: 'name', label: '姓名',align:'center',fixed: true, emptyString:'--' },
{ name: 'age', label: '年纪',sorter:false,align:'right',groupTitle: '分组二,分组一,总汇' },
{ name: 'sex', align:'center', label: '性别',filters:{'0':'男','1':'女'},groupTitle: '分组二,分组一,总汇'},
{ name: 'img', label: '图片',type:"img" },
{ name: 'address', label: '地址',align:'center',groupTitle: '分组三,分组一,总汇' },
{ name: 'date', label: '日期',sorter:true },
{ name: 'remark', label: '备注',groupTitle: '分组四,分组一,总汇' },
{ name: 'province', label: '省份',groupTitle: '分组四,分组一,总汇' },
{ name: 'other', label: '其他',groupTitle: '分组五,分组一,总汇' },
{ name: 'city', label: '城市',groupTitle: '总汇' },
{ name: 'zip', label: '邮编' },
{ name: 'operation', type:'operation',label: '操作',renders:[
{
name:'编辑',
func:'edit' // func 代表子元素点击的事件 父元素接收的事件 父元素 @edit
},
{
name:'删除',
type:'warn',
func:"dele"
},
]},
])
onMounted(() => {
})
getdatalist(unref(pageIndex))
</script>
<style lang="scss">
.next-table-container {
width: 100vw;
height: 100vh;
box-sizing: border-box;
}
.flex-btns {
display: flex;
position: fixed;
width: 100%;
left: 0;
bottom: 0;
}
</style>
```
## 示例demo(vue2)
``` html
<next-table
:show-header="true"
:columns="column"
:stripe="true"
:fit="false"
show-summary
sum-text="合计"
@rowClick="rowClick"
:summary-method="getSummaries"
@toggleRowSelection="toggleRowSelection"
@toggleAllSelection="toggleAllSelection"
:border="true"
@edit="buttonEdit"
@dele="dele"
:data="datalist"></next-table>
```
```js
<script>
export default {
data () {
return {
pageIndex: 1,
pageTotal: 5,
datalist: [],
checkNameList: [],
column: [
{ type:'selection', fixed:true,width:60 },
{ name: 'name', label: '姓名',fixed:false,width:80,emptyString:'--' },
{ name: 'age', label: '年纪',sorter:false,align:'right', },
{ name: 'sex', label: '性别',filters:{'0':'男','1':'女'}},
{ name: 'img', label: '图片',type:"img" },
{ name: 'address', label: '地址' },
{ name: 'date', label: '日期',sorter:true },
{ name: 'province', label: '省份' },
{ name: 'city', label: '城市' },
{ name: 'zip', label: '邮编' },
{ name: 'operation', type:'operation',label: '操作',renders:[
{
name:'编辑',
func:'edit' // func 代表子元素点击的事件 父元素接收的事件 父元素 @edit
},
{
name:'删除',
type:'warn',
func:"dele"
},
]
}
},
methods: {
getdatalist(pageIndex) {
const pageSize = 10
const arr = []
for(let i = pageSize*(pageIndex-1) + 1; i < pageSize*pageIndex; i++) {
arr.push({
date: '2023-06-23',
name: `刘先生${i}`,
province: '上海',
sex: i%2 ? '0' : '1',
checked: this.checkNameList[this.pageIndex] ? this.checkNameList[this.pageIndex].indexOf(`刘先生${i}`) !== -1 : false,
age: 20,
img: 'https://gss0.baidu.com/-Po3dSag_xI4khGko9WTAnF6hhy/zhidao/wh%3D450%2C600/sign=e58ae9feb1003af34defd464001aea6a/8601a18b87d6277f4d763bcf2f381f30e824fce5.jpg',
city: '广州市',
address: '天河区东圃镇2002号',
zip: 200333
})
}
this.datalist = arr
},
pageChange(index) {
this.pageIndex = index
this.getdatalist(this.pageIndex)
},
dele(item) {
const index = this.datalist.findIndex(it => it.name == item.name)
if (index != -1) {
this.datalist.splice(index, 1)
}
},
toggleAllSelection(_, list) {
this.checkNameList[this.pageIndex] = list.map(item => item.name)
},
toggleRowSelection(bool, list) {
this.checkNameList[this.pageIndex] = list.map(item => item.name)
},
buttonEdit(item) {
console.log(111111, item)
}
},
created() {
this.getdatalist(this.pageIndex)
}
}
</script>
```
## table 属性
| 参数 | 说明 | 类型 | 可选值 | 默认值 |是否必须|
| ------ | ------ | ------ | ------ | ------ |------ |
| data | 显示的数据 | array |-- | -- |必须 |
| column | 显示的列数据 | array |-- | -- |必须 |
| stripe | 是否为斑马纹 table| boolean | - |false | 否 |
| fit | 列的宽度是否自撑开 | boolean |true,false | false |否 |
| show-header | 是否显示表头 | boolean |true,false | true |否 |
| cell-style | 单元格的 style 的回调方法,也可以使用一个固定的 Object 为所有单元格设置一样的 Style。 | Function({row, column, rowIndex, columnIndex})/Object |-- | -- |否 |
| cell-header-style | 头部单元格的 style 的回调方法,也可以使用一个固定的 Object 为所有单元格设置一样的 Style。 | Function({ column, columnIndex})/Object |-- | -- |否 |
| formatter | colomn =》formatter 必须设置为true,才有作用,进行格式化数据,进行数据的转换 | Function({row, column, rowIndex, columnIndex})/Object |-- | -- |否 |
| border | 是否带有纵向边框 | boolean |true,false | true |否 |
| highlight | 是否要高亮当前行 | boolean |true,false | false |否 |
| show-summary | 是否在表尾显示合计行 | boolean |true,false | false |否 |
| sum-text | 合计行第一列的文本 | String |- | 合计 |否 |
| summary-method | 自定义的合计计算方法 | Function({ columns, data }) |- | - |否 |
| permissionBtn | 是否动态控制按钮的显示隐藏 | Function({ row, renders,index }) |- | - |否 |
| isShowLoadMore | 是否开启上拉加载 | boolean |true,false | false |否 |
| pullUpLoading | 开启上拉加载后的返回函数接收参数done是函数,done(type),type为空代表还有数据继续开启上拉加载type='ok',代表结束上拉加载 | Function(done) |-- | -- |否 |
| showPaging | 是否开启分页器 | boolean |true,false | false |否 |
| pageIndex | 开启分页器后,当前页码 | Number |-- | 1 |否 |
| pageTotal | 开启分页器后,总页数 | Number |-- | 0 |否 |
| primaryColor | 主题颜色(注意只支持16进制的颜色值如 #000000) | String |-- | 0 |#f0ad4e |
```
关闭上拉加载的方式1pullUpLoading((done)=>{
done(type)
})
done 接收参数为 type type为空代表还有数据可以继续加载无数据的时候传入 'ok'代表结束
```
## table 事件
| 参数 | 说明 | 类型 | 可选值 | 默认值 |是否必须|
| ------ | ------ | ------ |--------------------------| ------ |------ |
| 事件名自定义 | 取决于type类型为operation的 renders参数里面 func 的参数名 | Function | (row,index)=>{} | -- |否 |
| sort-change | 取决于type类型为operation的 renders参数里面 func 的参数名 | Function | (column,model,index)=>{} | -- |否 |
| currentChange | 当表格的当前行发生变化的时候会触发该事件,如果要高亮当前行,请打开表格的 highlight属性,this.$refs.table.resetHighlight()清除选中 | Function | (row,index)=>{} | -- |否 |
| toggleRowSelection | 用于多选表格,切换某一行的选中状态,第一个参数代表选中状态,参数二代表选中的对象 | Function | (selected ,array)=>{} | -- |否 |
| toggleAllSelection | 用于多选表格,切换所有行的选中状态 ,第一个参数代表选中状态,参数二代表选中的对象| Function | (selected ,array)=>{} | -- |否 |
| rowClick | 单击某行 第一个参数代表选中对象参数二代表选中的index| Function | (row ,index)=>{} | -- |否 |
| cellClick | 单击单元格 ,当某个单元格被点击时会触发该事件| Function | (row ,index,column)=>{} | -- |否 |
| pullUpLoading | 开启上拉加载后的返回函数,无参数| Function | -- |-- |否 |
| pageChange | 开起分页paging时候分页切换后的事件 返回切换后的页码 | Function | -- |-- |否 |
## data 属性
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| ------ | ------ | ------ | ------ | ------ |
| checked | 是否被勾选 | boolean |true,false | 无 |
## column 属性
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| ------ | ------ | ------ | ------ | ------ |
| name | 属性值 | string |-- | 无 |
| label | 显示的标题 | string |-- | 无 |
| width | 列的宽度 | number |-- | 100 |
| disabled | 是否禁用 | boolean |true,false | false |
| fixed | 列是否固定在左侧true 表示固定在左侧 | boolean |true,false | true |
| sorter | 排序当设置为custom的时候代表自定义排序不会再触发默认排序会触发table事件@sort-change,可以通过接口来进行排序 | boolean |true,false,'custom' | false |
| emptyString | 当值为空的时候默认显示的值 | string | | -- |
| filters | 对象过滤的选项,对象格式,对象中的元素需要有 key 和 value 属性。 | Object | {key:value} | -- |
| align | 对齐方式 | String | left/center/right | left |
| type | 为 operation 的时候代表为操作按钮,img的时候代表图片地址,index代表序列号 | string | operation,img,index | -- |
| renders | type 为operation的时候 必传 | Array | {name:'名字',func:"父元素接收事件名",type:"按钮的类型",size:"大小"} | -- |
```
type 为 operation 的时候代表为操作按钮
renders 代表传入的按钮 Array =>[
{
name:'编辑',
class:"", // 添加class
type:'primary',代表按钮的类型 type 为custom的时候自定义按钮 其他类型取决于uniapp buttom组件按钮
size:'mini',代表按钮的大小
func:'edit' // func 代表操作按钮点击的事件名字 父元素接收的事件 父元素 @edit
例如:// <next-table @edit=""/>
}
]
```

View File

@@ -10,7 +10,9 @@
], ],
"repository": "", "repository": "",
"engines": { "engines": {
"HBuilderX": "^4.36" "HBuilderX": "^4.36",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
}, },
"dcloudext": { "dcloudext": {
"type": "component-vue", "type": "component-vue",
@@ -30,55 +32,67 @@
"data": "无", "data": "无",
"permissions": "无" "permissions": "无"
}, },
"npmurl": "" "npmurl": "",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
}, },
"uni_modules": { "uni_modules": {
"dependencies": [], "dependencies": [],
"encrypt": [], "encrypt": [],
"platforms": { "platforms": {
"cloud": { "cloud": {
"tcb": "y", "tcb": "",
"aliyun": "y", "aliyun": "",
"alipay": "y" "alipay": ""
}, },
"client": { "client": {
"Vue": { "uni-app": {
"vue2": "y", "vue": {
"vue3": "y" "vue2": "-",
"vue3": "-"
}, },
"App": { "web": {
"app-vue": "y", "safari": "-",
"app-nvue": "u", "chrome": "-"
"app-uvue": "u",
"app-harmony": "u"
}, },
"H5-mobile": { "app": {
"Safari": "y", "vue": "-",
"Android Browser": "y", "nvue": "-",
"微信浏览器(Android)": "y", "android": "-",
"QQ浏览器(Android)": "y" "ios": "-",
"harmony": "-"
}, },
"H5-pc": { "mp": {
"Chrome": "y", "weixin": "-",
"IE": "u", "alipay": "-",
"Edge": "y", "toutiao": "-",
"Firefox": "y", "baidu": "-",
"Safari": "y" "kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-",
"xhs": "-"
}, },
"小程序": { "quickapp": {
"微信": "y", "huawei": "-",
"阿里": "u", "union": "-"
"百度": "u", }
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
}, },
"快应用": { "uni-app-x": {
"华为": "u", "web": {
"联盟": "u" "safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
} }
} }
} }

View File

@@ -13,7 +13,9 @@
], ],
"repository": "https://github.com/dcloudio/uni-ui", "repository": "https://github.com/dcloudio/uni-ui",
"engines": { "engines": {
"HBuilderX": "" "HBuilderX": "",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
}, },
"directories": { "directories": {
"example": "../../temps/example_temps" "example": "../../temps/example_temps"
@@ -36,49 +38,67 @@
"permissions": "无" "permissions": "无"
}, },
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue" "type": "component-vue",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
}, },
"uni_modules": { "uni_modules": {
"dependencies": [], "dependencies": [],
"encrypt": [], "encrypt": [],
"platforms": { "platforms": {
"cloud": { "cloud": {
"tcb": "y", "tcb": "",
"aliyun": "y", "aliyun": "",
"alipay": "n" "alipay": "x"
}, },
"client": { "client": {
"App": { "uni-app": {
"app-vue": "y", "vue": {
"app-nvue": "y" "vue2": "-",
"vue3": "-"
}, },
"H5-mobile": { "web": {
"Safari": "y", "safari": "-",
"Android Browser": "y", "chrome": "-"
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
}, },
"H5-pc": { "app": {
"Chrome": "y", "vue": "-",
"IE": "y", "nvue": "-",
"Edge": "y", "android": "-",
"Firefox": "y", "ios": "-",
"Safari": "y" "harmony": "-"
}, },
"小程序": { "mp": {
"微信": "y", "weixin": "-",
"阿里": "y", "alipay": "-",
"百度": "y", "toutiao": "-",
"字节跳动": "y", "baidu": "-",
"QQ": "y" "kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-",
"xhs": "-"
}, },
"快应用": { "quickapp": {
"华为": "u", "huawei": "-",
"联盟": "u" "union": "-"
}
}, },
"Vue": { "uni-app-x": {
"vue2": "y", "web": {
"vue3": "y" "safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
} }
} }
} }

View File

@@ -1,7 +1,6 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni"; import uni from "@dcloudio/vite-plugin-uni";
// https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [uni()], plugins: [uni()],
}); });