Compare commits

..

5 Commits

Author SHA1 Message Date
Ls
fb9a622a56 修改 2026-07-10 09:06:31 +08:00
Ls
d020483461 完整 2026-07-10 08:41:45 +08:00
Ls
d1eedfdc5b 保存 2026-05-26 08:41:00 +08:00
Ls
11486220aa 4-23 2026-04-23 10:07:44 +08:00
Ls
7dba9711a9 第一版 2026-04-16 08:38:54 +08:00
43 changed files with 15581 additions and 5031 deletions

303
AGENTS.md
View File

@@ -1,143 +1,212 @@
# 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
npm run dev:h5 # Run H5 development server
npm run dev:mp-weixin # Run WeChat mini-program development
npm run dev:app # Run app development
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
npm install
```
### Build Commands
## Build And Dev Commands
### Common Development
```bash
npm run build:h5 # Build for H5
npm run build:mp-weixin # Build for WeChat mini-program
npm run build:app # Build for app
npm run build:app-android # Build for Android
npm run build:app-ios # Build for iOS
npm run dev:h5
npm run dev:h5:ssr
npm run dev:app
npm run dev:app-android
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
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
- **Note**: No test framework configured in this project yet.
## 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
### Validation
```bash
npm run type-check
```
## 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/
├── pages/ # Page components (with sub-packages)
├── components/ # Reusable Vue components
├── Service/ # API services and utilities
├── common/ # Common utilities and helpers
├── static/ # Static assets (images, etc.)
├── types/ # TypeScript type definitions
├── uni_modules/ # Uni-app modules
── colorui/ # ColorUI CSS framework
├── Service/ Service classes and base config
├── common/ Shared utilities and domain models
├── components/ Reusable Vue SFC components
├── pages/ Routed uni-app pages
├── static/ Static assets
├── types/ Type declarations
├── uni_modules/ Third-party or packaged uni modules
── 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
- **Files**: PascalCase for components (e.g., `ImageCropper.vue`), camelCase for utilities
- **Components**: PascalCase for component names
- **Variables**: camelCase for local variables and functions
- **Constants**: UPPER_SNAKE_CASE for constants
- **Classes**: PascalCase for class names (e.g., `Service`)
- **CSS Classes**: kebab-case (e.g., `home-container`, `timer-card`)
## Imports And Module Usage
- Use the `@/` alias for imports rooted in `src/`.
- `tsconfig.json` maps `@/*` to `./src/*`.
- Prefer alias imports for app code instead of long relative paths.
- Import Vue composition helpers from `vue`.
- Import uni-app page lifecycle APIs from `@dcloudio/uni-app` when needed.
- 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
- Use `lang="ts"` in Vue SFC `<script>` tags
- Use `<script setup>` syntax for Vue 3 composition API
- Type function parameters and return values
- Use `any` sparingly, prefer proper type definitions
- Path alias: `@/*` maps to `./src/*`
## Vue SFC Conventions
- Use Vue 3 `<script setup lang="ts">` for page and component SFCs.
- Keep file sections in the order: `<template>`, `<script setup lang="ts">`, `<style lang="scss" scoped>`.
- Use `scoped` styles for page/component-local styling unless a global rule is required.
- Prefer reactive state via `ref` and `reactive` from Vue.
- Use uni-app page lifecycle hooks such as `onLoad` from `@dcloudio/uni-app`.
- Clean up timers or intervals in `onUnmounted()`.
### Vue Component Structure
```vue
<template>
<!-- Template with tabs for indentation -->
</template>
## TypeScript Guidelines
- Use `lang="ts"` in Vue SFC scripts.
- Add explicit parameter types and return types for exported functions and service methods where practical.
- Prefer concrete interfaces or domain models over `any`.
- 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">
// Imports first
// Composition API logic
// Functions
// Lifecycle hooks
</script>
## Service Layer Patterns
- Base configuration lives in `src/Service/BaseConfig.ts`.
- Shared service helpers live in `src/Service/Service.ts`.
- Feature services under `src/Service/swimming/` usually:
- define endpoint path constants as `private static` fields,
- 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>
/* SCSS styles with nested structure */
</style>
```
## Error Handling And User Feedback
- Use `Service.Msg()` for toast-style user feedback.
- 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
- **Indentation**: Use tabs (not spaces) for indentation
- **Quotes**: Single quotes for strings in TypeScript/JavaScript
- **Semicolons**: Optional but consistent (project uses semicolons)
- **Line endings**: CRLF (Windows)
- **SCSS**: Use nested selectors, kebab-case class names
## Naming Conventions
- Vue component filenames: PascalCase, for example `ImageCropper.vue`.
- Utility and service helper filenames: usually camelCase or existing established names; match nearby files.
- Service classes: PascalCase for class names, for example `PlanService`.
- Some existing service class identifiers start lowercase, such as `studentService`; preserve existing public names unless a broader refactor is requested.
- 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
- Use `Service.Msg()` for toast messages
- Use `Service.Alert()` for modal dialogs
- API requests through `Service.Request()` handle 401 (token expired) automatically
- Return `ResultData` objects from service methods
- Clean up intervals in `onUnmounted()` lifecycle hook
## Formatting Expectations
- Repository convention is tabs for indentation in source files.
- 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.
- Semicolons are used frequently enough that new changes should stay consistent within the edited file.
- Keep line endings as CRLF on Windows-oriented files when possible.
- Use nested SCSS selectors where that is already the local pattern.
- Avoid introducing broad formatting-only diffs.
### UI Components
- Use **uview-plus** as primary UI component library
- Use **ColorUI** for CSS framework and icons
- Component prefix: `u-` for uview-plus components (e.g., `<u-icon>`, `<u-button>`)
## Styling And UI
- Prefer `uview-plus` components where the project already uses them.
- ColorUI assets/components are also present; preserve established usage where relevant.
- 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
- Extend `BaseConfig` for service classes
- Use static methods for utility functions
- `Service` class provides:
- API requests: `Service.Request()`
- Navigation: `Service.GoPage()`, `Service.GoPageTab()`, `Service.GoPageBack()`
- Storage: `Service.SetStorageCache()`, `Service.GetStorageCache()`
- Messages: `Service.Msg()`, `Service.Alert()`
- Loading: `Service.LoadIng()`, `Service.LoadClose()`
## Platform And Runtime Notes
- Use `uni.*` APIs for platform-specific behavior.
- Navigation typically goes through `Service.GoPage()`, `Service.GoPageTab()`, `Service.GoPageBack()`, or `Service.GoPageDelse()`.
- App state and auth token access are commonly wrapped by `Service.GetStorageCache()` and related helpers.
- Be careful when changing files inside `src/uni_modules/`; many are third-party modules and should only be edited when necessary.
### uni-app Specifics
- Use `uni.` API for platform-specific operations
- Pages registered in `src/pages.json`
- Tab bar configured in `src/pages.json`
- Use `rpx` units for responsive design
- Global styles in `src/App.vue` and `src/uni.scss`
## Agent Guidance
- Before changing code, inspect nearby files and follow their existing conventions.
- Keep edits focused and minimal; do not refactor unrelated areas opportunistically.
- Do not invent lint or test commands that are not actually configured.
- When describing validation, use real commands from `package.json`.
- 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
- `.gitignore` includes `node_modules/`, `dist/`, `unpackage/`
- Commit messages should be descriptive (in Chinese or English)
### Key Dependencies
- Vue 3.4.21
- 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
## Current Gaps To Remember
- No lint pipeline is configured.
- No automated test pipeline is configured.
- No single-test command exists yet.
- No Cursor or Copilot repository instruction files exist.
- Agents should rely on local code patterns plus this file when making changes.

8
package-lock.json generated
View File

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

View File

@@ -1,11 +1,11 @@
export class BaseConfig {
protected static servesUrl: string = "http://192.168.0.142:5298";
protected static imgUrl: string = "http://192.168.0.142:5298";
protected static mediaUrl: string = "http://192.168.0.142:5298/";
// protected static servesUrl: string = "http://192.168.0.142:5300";
// protected static imgUrl: string = "http://192.168.0.142:5300";
// protected static mediaUrl: string = "http://192.168.0.142:5300/";
// protected static servesUrl: string = "http://vp.xypays.cn";
// protected static imgUrl: string = "http://vp.cloud.xypays.cn";
// protected static mediaUrl: string = "http://byc1.xypays.cn/";
protected static servesUrl: string = "https://swimming.api.xypays.cn";
protected static imgUrl: string = "https://swimming.api.xypays.cn";
protected static mediaUrl: string = "https://swimming.api.xypays.cn/";
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
protected static payuploadUrl: string = "http://192.168.0.142:5298";
protected static payuploadUrl: string = "https://swimming.api.xypays.cn";
}

View File

@@ -40,7 +40,84 @@ class PlanService {
return result;
}
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 {

View File

@@ -3,8 +3,8 @@ import { Service } from '@/Service/Service';
class studentService {
private static GetStudentListPath : string = '/api/Student/GetStudentList';
/*****学生列表不分页接口*****/
static GetStudentList() {
var result = Service.Request(this.GetStudentListPath, "GET", { });
static GetStudentList(planId:string) {
var result = Service.Request(this.GetStudentListPath, "GET", { planId });
return result;
}

View File

@@ -24,6 +24,38 @@ class userService {
}
private static GetUserListPath : string = '/api/User/GetUserList';
/*****查询用户列表接口*****/
static GetUserList(serch:string,page:number) {
var result = Service.Request(this.GetUserListPath, "GET", {serch,page});
return result;
}
private static UpdateUserRolePath : string = '/api/User/UpdateUserRole';
/*****更改用户权限接口*****/
static UpdateUserRole(userId:string) {
var result = Service.Request(this.UpdateUserRolePath, "POST", {userId});
return result;
}
private static GetKefuInfoPath : string = '/api/User/GetKefuInfo';
/*****获取客服信息接口*****/
static GetKefuInfo() {
var result = Service.Request(this.GetKefuInfoPath, "GET", {});
return result;
}
private static UpdateKefuPath : string = '/api/User/UpdateKefu';
/*****修改客服信息接口*****/
static UpdateKefu(phone:string,name:string) {
var result = Service.Request(this.UpdateKefuPath, "POST", {phone,name});
return result;
}
// private static GetStudentInfoPath : string = '/api/Student/GetStudentInfo';
// /*****根据学生id查详情接口*****/
// static GetStudentInfo(studentId:string) {

View File

@@ -34,7 +34,7 @@
{
"path": "swiming",
"style": {
"navigationBarTitleText": "游泳项目"
"navigationBarTitleText": "计时项目"
}
},
{
@@ -76,8 +76,9 @@
{
"path": "project",
"style": {
"navigationBarTitleText": "包干"
"navigationBarTitleText": "包干",
"pageOrientation": "auto",
"navigationStyle": "custom"
}
},
{
@@ -89,7 +90,9 @@
{
"path": "hunyang",
"style": {
"navigationBarTitleText": "混氧"
"navigationBarTitleText": "混氧",
"pageOrientation": "auto",
"navigationStyle": "custom"
}
},
{
@@ -97,12 +100,30 @@
"style": {
"navigationBarTitleText": "创建项目"
}
},
{
"path": "cs",
"style": {
"navigationBarTitleText": "测试",
"pageOrientation": "auto"
}
},
{
"path": "userPermission",
"style": {
"navigationBarTitleText": "权限管理"
}
},
{
"path": "contactManage",
"style": {
"navigationBarTitleText": "联系方式管理"
}
}
]
}, {
"root": "pages/dataAnalyze",
"pages": [
{
"pages": [{
"path": "baoganAnalyze",
"style": {
"navigationBarTitleText": "包干数据"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,30 @@
<template>
<view class="grades-container">
<!-- 页面标题区域 -->
<view class="header-section">
<view class="header-title">
<text class="title">成绩排名</text>
<text class="subtitle">学生最佳成绩排名</text>
<!-- 项目选择卡片 -->
<view class="filter-card ">
<view class="card-title">
<u-icon name="grid" size="18" color="#1890ff"></u-icon>
<text>选择项目</text>
</view>
<view class="picker-row" @click="showProject = true">
<text class="picker-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
<!-- ==================== 项目选择区域 ==================== -->
<view class="project-select-section">
<view class="section-header">
<text class="section-title">选择项目</text>
</view>
<!-- 项目选择器 -->
<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>
</view>
</picker>
</view>
<!-- ==================== 排名列表区域 ==================== -->
<!-- 仅在选择了项目且有数据时显示 -->
<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="stat-item">
<text class="stat-label">对比项目</text>
<text class="stat-value">{{ selectedProjectName }}</text>
<text class="stat-value">{{ selectProcet }}</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">参与人数</text>
<text class="stat-value">{{ gradeList.length }}</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">最高速度</text>
<text class="stat-value">{{ maxSpeed }}m/s</text>
<text class="stat-value">{{ gradeList.length }} </text>
</view>
</view>
@@ -49,25 +32,54 @@
<view class="ranking-list">
<view v-for="(item, index) in gradeList" :key="item.id"
:class="['ranking-item', { 'top-three': index < 3 }]" @click="handleRankingClick(item)">
<!-- 排名徽章 -->
<view class="rank-badge" :class="`rank-${index + 1}`">
<text class="rank-number">{{ index + 1 }}</text>
</view>
<!-- 学生信息 -->
<view class="student-info">
<text class="student-name">{{ item.name }}</text>
<text class="student-speed">最快速度{{ item.bestSpeed }}m/s</text>
<text class="student-name">{{ item.studentName }}</text>
<text class="student-speed">最快用时{{ formatTime(item.quicklyTime ) }}</text>
</view>
<!-- 成绩详情 -->
<view class="speed-detail">
<text class="detail-text">用时{{ item.bestTime }}s</text>
<text class="detail-text">日期{{ item.recordDate }}</text>
<text class="detail-text">{{ Service.formatDate(item.addTime,2) }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-card" v-if="selectProcet && gradeList.length === 0">
<u-icon name="file-text" size="64" color="#d9d9d9"></u-icon>
<text class="empty-text">暂无排名数据</text>
</view>
<!-- 项目选择弹窗 -->
<up-popup :show="showProject" mode="center" round="32" :safeAreaInsetBottom="false"
@close="showProject = false">
<view class="project-popup-content">
<view class="modal-header">
<text class="modal-title">选择项目</text>
<view class="modal-close" @click="showProject = false">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<scroll-view scroll-y class="popup-project-list">
<view class="project-list-inner">
<view v-for="(project, index) in projectOptions[0]" :key="project.planId" class="project-item"
@click="selectProcetFunc(project)">
<view class="item-info">
<text class="item-name">{{ project.name }}</text>
</view>
</view>
<view v-if="!projectOptions[0] || projectOptions[0].length === 0" class="empty-project">
<text class="empty-text">暂无项目</text>
</view>
</view>
</scroll-view>
</view>
</up-popup>
</view>
</template>
@@ -75,455 +87,430 @@
import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
// ==================== 响应式数据 - 项目相关 ====================
// 项目选择
let showProject = ref(false)
let selectProcet = ref('')
let selectId = ref('')
// 项目列表数据
const projectList = ref([
{ id: '1', name: '100米自由泳' },
{ id: '2', name: '50米自由泳' },
{ id: '3', name: '200米自由泳' },
{ id: '4', name: '100米蛙泳' }
])
const projectOptions = ref<Array<any>>([[]])
// 项目选择器选项(用于 picker 组件)
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 接口定义 ====================
/**
* 学生成绩接口
* 定义学生的成绩和排名信息
*/
interface StudentGrade {
id : string // 学生 ID
name : string // 学生姓名
bestSpeed : number // 最快速度m/s
bestTime : number // 最快用时(秒)
recordDate : string // 记录日期
studentId : string // 学生 ID用于数据关联
id: string
name: string
bestSpeed: number
bestTime: number
recordDate: string
studentId: string
}
// ==================== 响应式数据 - 排名相关 ====================
const gradeList = ref<Array<any>>([])
// 排名列表数据
const gradeList = ref<StudentGrade[]>([])
// 最高速度(计算属性)
const maxSpeed = computed(() => {
if (gradeList.value.length === 0) return 0
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' }
]
}
// ==================== 生命周期钩子 ====================
/**
* 页面加载时触发
* 在页面初始化时执行,只执行一次
*/
onLoad(() => {
// 初始化数据
loadProjectData()
getProjectData()
})
/**
* 页面显示时触发
* 每次页面从后台切换到前台时执行
*/
onShow(() => {
// TODO: 如果需要在页面显示时刷新数据,可以在这里添加逻辑
})
// ==================== 业务逻辑方法 - 项目相关 ====================
/**
* 加载项目数据
* 从后端 API 获取项目列表
*/
const loadProjectData = () => {
// TODO: 调用后端 API 获取项目列表
// 示例代码:
// Service.Request('/api/projects', 'GET').then(res => {
// projectList.value = res.data
// projectOptions.value = res.data
// })
const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => {
if (res.code == 0) {
projectOptions.value[0] = res.data.list
} else {
Service.Msg(res.msg)
}
})
}
/**
* 处理项目选择变化事件
* 当用户选择项目时触发
* @param e picker 选择事件对象,包含 detail.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 获取该项目的排名数据
const selectProcetFunc = (e: any) => {
selectProcet.value = e.name
selectId.value = e.planId
loadProjectGrades(selectId.value)
}
/**
* 加载项目排名数据
* 根据项目 ID 获取该项目的学生成绩排名列表
* @param projectId 项目 ID
*/
const loadProjectGrades = (projectId : string) => {
// 从模拟数据中获取排名列表
if (mockStudentGrades[projectId]) {
// 按最快速度从高到低排序
gradeList.value = [...mockStudentGrades[projectId]].sort((a, b) => b.bestSpeed - a.bestSpeed)
} else {
gradeList.value = []
}
const loadProjectGrades = (projectId: string) => {
PlanService.GetRankData(projectId).then(res => {
if (res.code == 0) {
gradeList.value = res.data.list
showProject.value=false
} else {
Service.Msg(res.msg)
}
})
}
// 格式化时间显示
const formatTime = (seconds : number) : string => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
const ms = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${ms.toString().padStart(2, '0')}`
}
// ==================== 业务逻辑方法 - 排名相关 ====================
/**
* 处理排名项点击事件
* 点击某排名项查看详情
* @param item 排名项数据
*/
const handleRankingClick = (item : StudentGrade) => {
const handleRankingClick = (item: StudentGrade) => {
console.log('点击了排名项:', item)
// TODO: 跳转到学生详情页面
// Service.GoPage('/pages/student/detail', { studentId: item.studentId })
}
</script>
<style lang="scss" scoped>
// 页面背景色设置
page {
background-color: #f5f5f5;
background-color: #e8ecf3;
}
// 页面容器
.grades-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
/* ==================== 页面标题区域 ==================== */
.header-section {
background-color: #fff;
padding: 32rpx 28rpx 24rpx;
/* 通用卡片 */
.filter-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.header-title {
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
.card-title {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.picker-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 24rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 1rpx solid #e4e8ee;
transition: all 0.2s;
&:active {
background: #eef1f6;
}
.picker-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
&.placeholder {
color: #999;
font-weight: 400;
}
}
}
/* 排名区域 */
.ranking-section {
margin-top: 4rpx;
}
.stats-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-around;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.stat-item {
text-align: center;
.stat-label {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
font-weight: 500;
}
.subtitle {
font-size: 24rpx;
color: #999;
.stat-value {
font-size: 32rpx;
font-weight: 700;
color: #faad14;
}
}
.stat-divider {
width: 1rpx;
height: 50rpx;
background-color: #f0f0f0;
}
}
/* ==================== 项目选择区域 ==================== */
.project-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;
transition: all 0.3s ease;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx 0 0 20rpx;
}
.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: #fff0f5;
border-color: #1890ff;
transform: scale(0.98);
}
.picker-text {
font-size: 28rpx;
color: #333;
}
}
}
/* ==================== 排名列表区域 ==================== */
.ranking-section {
margin: 0 20rpx;
// 统计卡片
.stats-card {
.ranking-list {
.ranking-item {
background-color: #fff;
border-radius: 20rpx;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
justify-content: space-around;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
gap: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx 0 0 20rpx;
&:active {
transform: scale(0.99);
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
}
.stat-item {
text-align: center;
.stat-label {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.stat-value {
font-size: 32rpx;
font-weight: 700;
color: #1890ff;
}
&.top-three {
border-left-color: #ffd700;
}
.stat-divider {
width: 1rpx;
height: 50rpx;
background-color: #eee;
}
}
// 排名列表容器
.ranking-list {
// 排名项
.ranking-item {
background-color: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
.rank-badge {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
justify-content: center;
flex-shrink: 0;
background-color: #f0f0f0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 20rpx 0 0 20rpx;
.rank-number {
font-size: 24rpx;
font-weight: 700;
color: #999;
}
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 16rpx rgba(235, 47, 150, 0.15);
&::before {
opacity: 1;
}
}
// 排名徽章
.rank-badge {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: #f0f0f0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&.rank-1 {
background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
box-shadow: 0 2rpx 8rpx rgba(255, 215, 0, 0.4);
.rank-number {
font-size: 24rpx;
font-weight: 700;
color: #999;
}
// 前三名特殊颜色
&.rank-1 {
background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
box-shadow: 0 2rpx 8.6rpx rgba(255, 215, 0, 0.4);
.rank-number {
color: #fff;
font-size: 28rpx;
}
}
&.rank-2 {
background: linear-gradient(135deg, #c0c0c0 0%, #d9d9d9 100%);
box-shadow: 0 2rpx 8rpx rgba(192, 192, 192, 0.4);
.rank-number {
color: #fff;
}
}
&.rank-3 {
background: linear-gradient(135deg, #cd7f32 0%, #e6963d 100%);
box-shadow: 0 2rpx 8rpx rgba(205, 127, 50, 0.4);
.rank-number {
color: #fff;
}
color: #fff;
font-size: 28rpx;
}
}
// 学生头像
.student-avatar {
width: 70rpx;
height: 70rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2rpx 8rpx rgba(235, 47, 150, 0.3);
&.rank-2 {
background: linear-gradient(135deg, #c0c0c0 0%, #d9d9d9 100%);
box-shadow: 0 2rpx 8rpx rgba(192, 192, 192, 0.4);
.avatar-text {
font-size: 28rpx;
font-weight: 700;
.rank-number {
color: #fff;
}
}
// 学生信息
.student-info {
flex: 1;
min-width: 0;
&.rank-3 {
background: linear-gradient(135deg, #cd7f32 0%, #e6963d 100%);
box-shadow: 0 2rpx 8rpx rgba(205, 127, 50, 0.4);
.student-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.student-speed {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
.rank-number {
color: #fff;
}
}
}
// 成绩详情
.speed-detail {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
.student-info {
flex: 1;
min-width: 0;
.detail-text {
font-size: 24rpx;
color: #999;
background-color: #f5f5f5;
padding: 6rpx 12rpx;
border-radius: 6rpx;
}
.student-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.student-speed {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
}
.speed-detail {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
.detail-text {
font-size: 24rpx;
color: #999;
background-color: #f5f7fa;
padding: 6rpx 12rpx;
border-radius: 6rpx;
}
}
}
}
</style>
/* 空状态 */
.empty-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin-top: 4rpx;
.empty-text {
margin-top: 24rpx;
font-size: 28rpx;
color: #999;
}
}
/* 项目选择弹窗 */
.project-popup-content {
width: 680rpx;
max-height: 70vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.25), 0 0 0 1rpx rgba(0, 0, 0, 0.04);
}
.modal-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);
.modal-title {
font-size: 36rpx;
font-weight: 700;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.modal-close {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
&:active {
background: linear-gradient(135deg, #e8e8e8 0%, #e0e0e0 100%);
transform: scale(0.92);
}
}
}
.popup-project-list {
max-height: 55vh;
padding: 24rpx 28rpx;
}
.project-list-inner {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.project-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
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;
border-radius: 16rpx;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4rpx;
height: 100%;
background: linear-gradient(180deg, #1890ff 0%, #40a9ff 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::after {
opacity: 1;
}
.item-icon {
flex-shrink: 0;
.icon-bg {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #36cfc9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.25);
position: relative;
&::before {
content: '';
position: absolute;
top: 6rpx;
right: 6rpx;
width: 10rpx;
height: 10rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.icon-text {
font-size: 28rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
}
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@
<view class="empty-state-container">
<!-- 标题区域 -->
<view class="title-section">
<text class="main-title">暂无项目</text>
<text class="sub-title">您还没有创建任何训练项目开始您的第一次游泳训练记录吧</text>
<!-- <text class="main-title">暂无项目</text>
<text class="sub-title">您还没有创建任何训练项目开始您的第一次游泳训练记录吧</text> -->
</view>
<!-- 创建项目模块列表 -->
@@ -21,7 +21,6 @@
</view>
<view class="card-info">
<text class="card-title">计时项目</text>
<text class="card-desc">记录单次训练的计时数据</text>
</view>
<view class="card-arrow">
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
@@ -29,7 +28,7 @@
</view>
<!-- 包干项目模块 -->
<view @click="Service.GoPage('/pages/userFunc/project')" class="create-card package-card">
<view @click=" goBaogan()" class="create-card package-card">
<view class="card-icon">
<view class="icon-circle package-icon">
<u-icon name="grid" size="40" color="#fff"></u-icon>
@@ -37,7 +36,6 @@
</view>
<view class="card-info">
<text class="card-title">包干项目</text>
<text class="card-desc">记录固定距离的训练成绩</text>
</view>
<view class="card-arrow">
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
@@ -53,7 +51,6 @@
</view>
<view class="card-info">
<text class="card-title">分段项目</text>
<text class="card-desc">记录分段训练的详细数据</text>
</view>
<view class="card-arrow">
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
@@ -69,13 +66,16 @@
</view>
<view class="card-info">
<text class="card-title">混氧项目</text>
<text class="card-desc">记录混氧训练的计时数据</text>
</view>
<view class="card-arrow">
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
</view>
</view>
</view>
<!-- <view @click="Service.GoPage('/pages/userFunc/cs')" class="">
cs
</view> -->
</view>
<!-- 计时项目列表弹窗 -->
@@ -97,12 +97,13 @@
<!-- 项目列表 -->
<view class="project-list">
<text class="list-title" v-if="projects.length > 0">项目列表</text>
<view v-if="projects.length > 0" class="list-container">
<view v-for="project in projects" :key="project.planId" class="project-item"
<scroll-view v-if="projects.length > 0" scroll-y="true" @scrolltolower='getList()'
class="list-container">
<view v-for="(project,index) in projects" :key="project.planId" class="project-item"
@click="handleProjectClick(project)">
<view class="item-icon">
<view class="icon-bg">
<text class="icon-text">{{ project.name.charAt(0) }}</text>
<text class="icon-text">{{ index+1 }}</text>
</view>
</view>
<view class="item-info">
@@ -110,7 +111,7 @@
<text v-if="currentIndex!==3" class="item-count">{{ project.users.length }}</text>
</view>
</view>
</view>
</scroll-view>
<view v-else class="empty-project">
<text class="empty-text">暂无项目</text>
</view>
@@ -125,30 +126,41 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
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 { PlanService } from '@/Service/swimming/PlanService'
import { userService } from '@/Service/swimming/userService'
let currentIndex = ref(0)
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 模拟项目数据
const projects = ref<Array<any>>([])
// 弹窗状态
const showTimingModal = ref(false)
let isUse = ref(false)
onLoad(() => {
if (!Service.GetUserToken()) {
login()
} else {
isUseFunc()
}
})
onShow(() => {
showTimingModal.value = false
uni.showTabBar()
})
const login = () => {
uni.getProvider({
service: 'oauth',
@@ -162,6 +174,7 @@
).then(content => {
if (content.code == 0) {
Service.SetUserToken(content.data.token)
isUseFunc()
} else {
Service.Msg(content.msg)
}
@@ -174,7 +187,19 @@
}
});
}
const isUseFunc = () => {
userService.IsCheakUserVerify().then(res => {
if (res.code == 0) {
isUse.value = res.data.isCheak
} else {
Service.Msg(res.msg)
}
})
}
// 获取项目列表数据
const getData = () => {
projects.value = []
@@ -182,14 +207,15 @@
status.value = 'loadmore'
getList()
}
// 获取项目列表
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
PlanService.GetPlanList(currentIndex.value==1?'计时项目':(currentIndex.value==2?'分段项目':'混氧项目'), page.value.toString()).then(res => {
PlanService.GetPlanList(currentIndex.value == 1 ? '计时项目' : (currentIndex.value == 2 ? '分段项目' : '混氧项目'), page.value.toString()).then(res => {
if (res.code == 0) {
projects.value = [...projects.value, ...res.data]
status.value = res.data.length == 10 ? 'loadmore' : 'nomore'
@@ -199,12 +225,24 @@
}
})
}
const goBaogan = () => {
if (!isUse.value) {
Service.Msg('用户暂无权限,请等待审核!')
return
}
Service.GoPage('/pages/userFunc/project')
}
// 显示计时项目列表弹窗
const showTimingProjectModal = (index : any) => {
uni.hideTabBar()
if (!isUse.value) {
Service.Msg('用户暂无权限,请等待审核!')
return
}
uni.hideTabBar()
showTimingModal.value = true
currentIndex.value = index
getData()
@@ -228,20 +266,21 @@
}
// 处理项目点击事件
const handleProjectClick = (project:any) => {
const handleProjectClick = (project : any) => {
if (currentIndex.value === 1) {
showTimingModal.value = false
Service.GoPage('/pages/userFunc/swiming?id='+project.planId)
Service.GoPage('/pages/userFunc/swiming?id=' + project.planId)
}
else if (currentIndex.value === 2) {
showTimingModal.value = false
Service.GoPage('/pages/userFunc/segmentation?id='+project.planId)
Service.GoPage('/pages/userFunc/segmentation?id=' + project.planId)
}
else {
showTimingModal.value = false
Service.GoPage('/pages/userFunc/hunyang?id='+project.planId)
Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId)
}
}
</script>
@@ -524,6 +563,7 @@
backdrop-filter: blur(10rpx);
z-index: 998;
animation: fadeIn 0.3s ease;
height: 100vh;
}
.modal-header {
@@ -571,7 +611,7 @@
transform: translate(-50%, -50%);
width: 90%;
max-width: 680rpx;
max-height: 80vh;
height: 80vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx;
z-index: 999;
@@ -652,6 +692,7 @@
display: flex;
flex-direction: column;
gap: 16rpx;
max-height: 800rpx;
}
.project-item {
@@ -660,7 +701,7 @@
gap: 20rpx;
padding: 28rpx 24rpx;
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);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
position: relative;

View File

@@ -6,7 +6,8 @@
<view class="avatar-circle" v-if="!userInfo.headImg">
<u-icon name="account" size="52" color="#fff"></u-icon>
</view>
<image v-else class="avatar-image" :src="Service.GetMateUrlByImg(userInfo.headImg)" mode="aspectFill"></image>
<image v-else class="avatar-image" :src="Service.GetMateUrlByImg(userInfo.headImg)" mode="aspectFill">
</image>
</view>
<view class="user-info">
<text class="user-nickname">{{ userInfo.name }}</text>
@@ -23,16 +24,15 @@
<view class="stats-section">
<view class="stats-card">
<view class="stat-item">
<view class="stat-info">
<text class="stat-value">{{ userInfo.projectCount || 0 }}</text>
<view @click="goPage('/pages/userFunc/projectList')" class="stat-info">
<text class="stat-value">{{ planCount }}</text>
<text class="stat-label">我的项目</text>
</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<view class="stat-info">
<text class="stat-value">{{ userInfo.studentCount || 0 }}</text>
<view @click="goPage('/pages/userFunc/student')" class="stat-info">
<text class="stat-value">{{ studentCount }}</text>
<text class="stat-label">学员数</text>
</view>
</view>
@@ -43,7 +43,7 @@
<view class="menu-section">
<view class="menu-card">
<!-- 项目管理菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/projectList')">
<view class="menu-item" @click="goPage('/pages/userFunc/projectList')">
<view class="menu-icon-bg project-icon-bg">
<u-icon name="list" size="28" color="#fff"></u-icon>
</view>
@@ -65,7 +65,7 @@
<!-- 学员管理菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/student')">
<view class="menu-item" @click="goPage('/pages/userFunc/student')">
<view class="menu-icon-bg academy-icon-bg">
<u-icon name="grid" size="28" color="#fff"></u-icon>
</view>
@@ -86,7 +86,7 @@
<view class="menu-divider"></view>
<!-- 数据分析菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/analyze')">
<view class="menu-item" @click="goPage('/pages/userFunc/analyze')">
<view class="menu-icon-bg analysis-icon-bg">
<u-icon name="order" size="28" color="#fff"></u-icon>
</view>
@@ -105,6 +105,56 @@
</view>
</view>
</view>
<!-- 管理员功能区域 -->
<view class="menu-section" v-if="userInfo.role==1">
<view class="menu-card">
<!-- 权限管理菜单项 -->
<view class="menu-item" @click="goPage('/pages/userFunc/userPermission')">
<view class="menu-icon-bg admin-icon-bg">
<u-icon name="lock" size="28" color="#fff"></u-icon>
</view>
<view class="menu-content">
<view class="menu-header">
<text class="menu-title">权限管理</text>
<view class="menu-tag admin-tag">
<text class="tag-text">管理员</text>
</view>
</view>
<text class="menu-desc">管理用户权限和角色</text>
</view>
<view class="menu-arrow">
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
<view class="menu-divider"></view>
<!-- 联系方式管理菜单项 -->
<view class="menu-item" @click="goPage('/pages/userFunc/contactManage')">
<view class="menu-icon-bg contact-icon-bg">
<u-icon name="phone" size="28" color="#fff"></u-icon>
</view>
<view class="menu-content">
<view class="menu-header">
<text class="menu-title">联系方式管理</text>
<view class="menu-tag contact-tag">
<text class="tag-text">管理员</text>
</view>
</view>
<text class="menu-desc">管理客服联系方式</text>
</view>
<view class="menu-arrow">
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
</view>
</view>
<!-- 底部联系信息 -->
<view v-if="userInfo.role!=1" class="contact-footer" @click="callContact">
<text class="contact-label">客服联系人{{ kefuName }}</text>
<text class="contact-phone">{{ kefuPhone }}</text>
</view>
</view>
</template>
@@ -114,48 +164,62 @@
import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService'
let planCount = ref(0)
let studentCount = ref(0)
let userInfo = ref<any>({})
let isUse = ref(false)
let kefuName = ref('')
let kefuPhone = ref('')
onLoad(() => {
loadUserInfo()
})
onShow(() => {
loadUserInfo()
isUseFunc()
})
const isUseFunc = () => {
userService.IsCheakUserVerify().then(res => {
if (res.code == 0) {
isUse.value = res.data.isCheak
} else {
Service.Msg(res.msg)
}
})
}
const loadUserInfo = () => {
userService.GetUserInfo().then((content) => {
userService.GetUserInfo().then((content : any) => {
if (content.code == 0) {
userInfo.value=content.data.userInfo
studentCount.value = content.data.studentCount
planCount.value = content.data.planCount
userInfo.value = content.data.userInfo
kefuName.value = content.data.kefuName
kefuPhone.value = content.data.kefuPhone
} else {
Service.Msg(content.msg)
}
})
}
/**
* 跳转到训练计划页面
* 点击"训练计划"菜单项时触发
* 功能说明:
* - 跳转到训练计划列表页面
* - 用户可以查看、添加、编辑训练计划
*/
const goToTrainingPlans = () => {
Service.GoPage('/pages/userFunc/plan')
const goPage = (path : any) => {
if (!isUse.value) {
Service.Msg('用户暂无权限,请等待审核!')
return
}
Service.GoPage(path)
}
// 跳转到设置
const goToSettings = () => {
Service.Msg('系统设置功能开发中')
}
// 跳转到关于
const goToAbout = () => {
Service.Msg('关于我们功能开发中')
const callContact = () => {
uni.makePhoneCall({
phoneNumber: kefuPhone.value,
fail: () => {
Service.Msg('拨打电话失败')
}
})
}
</script>
@@ -167,7 +231,7 @@
.user-container {
padding: 20rpx;
padding-top: 30rpx;
padding-bottom: 40rpx;
}
/* 用户信息卡片 */
@@ -315,12 +379,6 @@
align-items: center;
justify-content: center;
gap: 16rpx;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
}
.stat-icon-bg {
@@ -459,6 +517,16 @@
background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%);
}
/* 权限管理图标背景色 - 红色渐变 */
&.admin-icon-bg {
background: linear-gradient(135deg, #f5222d 0%, #ff4d4f 50%, #cf1322 100%);
}
/* 联系方式管理图标背景色 - 青色渐变 */
&.contact-icon-bg {
background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 50%, #08979c 100%);
}
&.settings-icon-bg {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
}
@@ -515,6 +583,13 @@
color: #faad14;
}
/* 管理员标签样式 - 红色 */
&.admin-tag,
&.contact-tag {
background: linear-gradient(135deg, rgba(245, 34, 45, 0.12) 0%, rgba(245, 34, 45, 0.08) 100%);
color: #f5222d;
}
.tag-text {
font-size: 20rpx;
font-weight: 500;
@@ -555,4 +630,29 @@
margin: 0 32rpx;
}
}
/* 底部固定联系信息 */
.contact-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 16rpx 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
z-index: 10;
.contact-label {
font-size: 24rpx;
color: #999;
}
.contact-phone {
font-size: 26rpx;
color: #1890ff;
}
}
</style>

View File

@@ -10,29 +10,44 @@
<input class="form-input" v-model="projectName" placeholder="请输入项目名称"
placeholder-class="input-placeholder" />
</view>
<!-- 计划总时长 -->
<view class="total-time-section">
<view class="total-time-label">计划总时长</view>
<view class="total-time-value">{{ formatTotalTime(totalDuration) }}</view>
</view>
</view>
<!-- 计划列表 -->
<view class="form-card">
<view class="form-title">计划列表</view>
<!-- 分组列表 -->
<view class="form-card" v-for="(group, groupIndex) in groups" :key="group.id">
<view class="group-header">
<view class="group-title-wrapper">
<text class="form-title">{{ group.name || `分组 ${groupIndex + 1}` }}</text>
<view class="delete-group-btn" @click="deleteGroup(groupIndex)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
<!-- 分组名称输入 -->
<view class="form-group" style="margin-bottom: 20rpx;">
<text class="form-label">分组名称</text>
<input class="form-input" v-model="group.name" placeholder="请输入分组名称"
placeholder-class="input-placeholder" />
</view>
<!-- 计划项列表 -->
<view v-for="(item, index) in planList" :key="index" class="plan-item">
<view v-for="(item, planIndex) in group.plans" :key="planIndex" class="plan-item">
<view class="plan-item-header">
<text class="plan-item-title">计划 {{ index + 1 }}</text>
<view class="delete-plan-btn" @click="deletePlanItem(index)">
<text class="plan-item-title">计划 {{ planIndex + 1 }}</text>
<view class="delete-plan-btn" @click="deletePlanItem(groupIndex, planIndex)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
<view class="plan-item-content">
<!-- 目标时间 - 分秒选择 -->
<!-- 计划名称 -->
<view class="input-group">
<text class="input-label">计划名称</text>
<input class="plan-name-input" v-model="item.name" placeholder="请输入计划名称" />
</view>
<!-- 目标时间 - 秒输入 -->
<view class="input-group">
<text class="input-label">目标时间</text>
<view class="time-selector">
@@ -43,6 +58,14 @@
</view>
</view>
</view>
<!-- 单项时长显示 -->
<!-- <view class="input-group" v-if="item.allTime > 0">
<text class="input-label">单项时长</text>
<view class="all-time-display">
<text class="all-time-value">{{ item.allTime }} </text>
</view>
</view> -->
<!-- 休息时长 - 秒选择 -->
<view class="input-group">
@@ -65,13 +88,21 @@
</view>
<!-- 添加计划按钮 -->
<view class="add-plan-btn" @click="addPlanItem">
<view class="add-plan-btn" @click="addPlanItem(groupIndex)">
<u-icon name="plus-circle" size="20" color="#1890ff"></u-icon>
<text class="add-plan-text">添加计划</text>
</view>
</view>
</view>
<!-- 添加分组按钮 -->
<view class="add-group-btn" @click="addGroup">
<u-icon name="plus-circle" size="24" color="#fff"></u-icon>
<text class="add-group-text">添加分组</text>
</view>
</view>
<view class="" style="width: 100%; height: 80rpx;" >
</view>
<!-- 底部按钮区域 -->
<view class="bottom-actions">
<view class="action-buttons">
@@ -85,50 +116,79 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Service } from '@/Service/Service'
import { studentService } from '@/Service/swimming/studentService'
import { PlanService } from '@/Service/swimming/PlanService'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { onLoad } from '@dcloudio/uni-app'
// 计划项接口
interface PlanItem {
targetMinutes : string
targetSeconds : string
restTime : string
lapCount : string
name: string
targetSeconds: string
restTime: string
lapCount: string
allTime?: number
}
// 分组接口
interface Group {
id: string
name: string
plans: PlanItem[]
allTime?: number
}
// 项目名称
const projectName = ref('')
// 计划列表
const planList = ref<PlanItem[]>([])
// 分组列表
const groups = ref<Group[]>([])
let planId = ref('')
onLoad((data : any) => {
onLoad((data: any) => {
planId.value = data.id
if(planId.value ){
if (planId.value) {
getPlanInfo()
} else {
// 新建时默认添加一个分组
addGroup()
}
})
// 获取计划详情
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
projectName.value=res.data.plan.name
JSON.parse(res.data.plan.project).map((item:any)=>{
planList.value.push({
targetMinutes:'',
targetSeconds : item.target,
restTime : item.rest,
lapCount : item.circle
})
})
projectName.value = res.data.plan.name
const projectData = JSON.parse(res.data.plan.project)
// 兼容旧数据:如果 project 是数组,放入一个默认分组
if (Array.isArray(projectData)) {
groups.value = projectData.map((g: any, index: number) => ({
id: generateId(),
name: g.groupName || `分组 ${index + 1}`,
allTime: g.allTime || 0,
plans: (g.group || []).map((item: any) => ({
name: String(item.name || ''),
targetSeconds: String(item.target || ''),
restTime: String(item.rest || ''),
lapCount: String(item.circle || ''),
allTime: item.allTime || 0
}))
}))
} else if (projectData.groups && Array.isArray(projectData.groups)) {
// 新数据格式
groups.value = projectData.groups.map((g: any) => ({
id: g.id || generateId(),
name: g.name || '',
allTime: g.allTime || 0,
plans: (g.plans || []).map((item: any) => ({
name: String(item.name || ''),
targetSeconds: String(item.target || ''),
restTime: String(item.rest || ''),
lapCount: String(item.circle || ''),
allTime: item.allTime || 0
}))
}))
}
} else {
// 显示错误信息
Service.Msg(res.msg)
}
})
@@ -137,55 +197,75 @@
// 计算计划总时长(秒)
const totalDuration = computed(() => {
let total = 0
planList.value.forEach(item => {
const targetMinutes = parseInt(item.targetMinutes) || 0
const targetSeconds = parseInt(item.targetSeconds) || 0
const targetTime = targetMinutes * 60 + targetSeconds
const restTime = parseInt(item.restTime) || 0
const lapCount = parseInt(item.lapCount) || 0
// 计算公式:(目标时间 + 休息时间) * 圈数
total += (targetTime + restTime) * lapCount
groups.value.forEach(group => {
group.plans.forEach(item => {
const targetSeconds = parseInt(item.targetSeconds) || 0
const restTime = parseInt(item.restTime) || 0
const lapCount = parseInt(item.lapCount) || 0
// 计算公式:(目标时间 + 休息时间) * 圈数
total += (targetSeconds + restTime) * lapCount
})
})
return total
})
// 格式化总时长显示(时:分:秒)
const formatTotalTime = (seconds : number) : string => {
const formatTotalTime = (seconds: number): string => {
if (seconds <= 0) return '00:00:00'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60)
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// 生成唯一ID
const generateId = () : string => {
const generateId = (): string => {
return Date.now().toString() + Math.random().toString(36).substr(2, 9)
}
// 添加计划项
const addPlanItem = () => {
const newItem : PlanItem = {
// 添加分组
const addGroup = () => {
const newGroup: Group = {
id: generateId(),
targetMinutes: '',
name: '',
plans: []
}
groups.value.push(newGroup)
}
// 删除分组
const deleteGroup = (groupIndex: number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除该分组吗?分组内的所有计划也将被删除。',
success: (res) => {
if (res.confirm) {
groups.value.splice(groupIndex, 1)
Service.Msg('删除成功')
}
}
})
}
// 添加计划项
const addPlanItem = (groupIndex: number) => {
const newItem: PlanItem = {
name: '',
targetSeconds: '',
restTime: '',
lapCount: ''
}
planList.value.push(newItem)
groups.value[groupIndex].plans.push(newItem)
}
// 删除计划项
const deletePlanItem = (index : number) => {
const deletePlanItem = (groupIndex: number, planIndex: number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除该计划吗?',
success: (res) => {
if (res.confirm) {
planList.value.splice(index, 1)
groups.value[groupIndex].plans.splice(planIndex, 1)
Service.Msg('删除成功')
}
}
@@ -193,79 +273,128 @@
}
// 验证表单
const validateForm = () : boolean => {
const validateForm = (): boolean => {
if (!projectName.value.trim()) {
Service.Msg('请输入项目名称')
return false
}
if (planList.value.length === 0) {
Service.Msg('请至少添加一个计划')
if (groups.value.length === 0) {
Service.Msg('请至少添加一个分组')
return false
}
for (let i = 0; i < planList.value.length; i++) {
const item = planList.value[i]
if (!item.targetSeconds) {
Service.Msg(`计划 ${i + 1} 请输入目标时间`)
for (let g = 0; g < groups.value.length; g++) {
const group = groups.value[g]
if (!group.name.trim()) {
Service.Msg(`分组 ${g + 1} 请输入分组名称`)
return false
}
const seconds = parseInt(item.targetSeconds) || 0
if (seconds === 0) {
Service.Msg(`计划 ${i + 1} 目标时间不能为0`)
if (group.plans.length === 0) {
Service.Msg(`分组 "${group.name}" 请至少添加一个计划`)
return false
}
if (!item.restTime || parseInt(item.restTime) < 0) {
Service.Msg(`计划 ${i + 1} 请输入有效的休息时长`)
return false
}
if (!item.lapCount || parseInt(item.lapCount) <= 0) {
Service.Msg(`计划 ${i + 1} 请输入有效的圈数`)
return false
for (let i = 0; i < group.plans.length; i++) {
const item = group.plans[i]
if (!item.name.trim()) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入计划名称`)
return false
}
if (!item.targetSeconds) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入目标时间`)
return false
}
const seconds = parseInt(item.targetSeconds) || 0
if (seconds === 0) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 目标时间不能为0`)
return false
}
if (!item.restTime || parseInt(item.restTime) < 0) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入有效的休息时长`)
return false
}
if (!item.lapCount || parseInt(item.lapCount) <= 0) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入有效的圈数`)
return false
}
}
}
return true
}
// 计算单项时长
const calculatePlanAllTime = (item: PlanItem): number => {
const target = parseInt(item.targetSeconds) || 0
const rest = parseInt(item.restTime) || 0
const circle = parseInt(item.lapCount) || 0
return (target + rest) * circle
}
// 计算分组总时长
const calculateGroupAllTime = (group: Group): number => {
return group.plans.reduce((sum, item) => sum + calculatePlanAllTime(item), 0)
}
// 保存
const handleSave = () => {
if (!validateForm()) {
return
}
let plan = planList.value.map(item => ({
target: (parseInt(item.targetMinutes) || 0) * 60 + (parseInt(item.targetSeconds) || 0),
rest: parseInt(item.restTime),
circle: parseInt(item.lapCount)
}))
// 整理数据
const projectData = groups.value.map(group => {
const groupAllTime = calculateGroupAllTime(group)
return {
allTime: groupAllTime,
groupName: group.name,
group: group.plans.map(item => ({
allTime: calculatePlanAllTime(item),
name: item.name,
target: parseInt(item.targetSeconds) || 0,
rest: parseInt(item.restTime) || 0,
circle: parseInt(item.lapCount) || 0
}))
}
})
const data = {
planId: planId.value,
name: projectName.value,
planType: '混氧项目',
departType: '',
interval: '',
groupInt: '',
groupInt: String(groups.value.length),
subsectionDistance: '',
subsectionInt: '',
users: '',
project: JSON.stringify(plan),
group: '',
project: JSON.stringify(projectData),
group: ''
}
console.log('保存的数据:', data)
PlanService.AddPlan(data).then(res => {
if (res.code == 0) {
Service.Msg('添加成功!')
setTimeout(() => {
Service.GoPageBack()
}, 1000)
Service.Msg(planId.value?'修改成功!':'添加成功!')
if(planId.value){
setTimeout(() => {
Service.GoPageBack()
}, 1000)
uni.$emit('change')
}else{
setTimeout(() => {
Service.GoPageDelse('/pages/userFunc/hunyang?id=' + res.data.planId)
}, 1000)
}
} else {
Service.Msg(res.msg)
}
@@ -317,6 +446,38 @@
}
}
/* 分组头部 */
.group-header {
margin-bottom: 20rpx;
.group-title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.form-title {
margin-bottom: 0;
}
}
}
/* 删除分组按钮 */
.delete-group-btn {
width: 44rpx;
height: 44rpx;
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
}
/* 表单输入 */
.form-group {
margin-bottom: 0;
@@ -496,6 +657,45 @@
}
}
/* 计划名称输入 */
.plan-name-input {
flex: 1;
font-size: 28rpx;
color: #333;
background-color: #fff;
border-radius: 12rpx;
padding: 0 16rpx;
height: 72rpx;
}
/* 分组总时长 */
.group-time-info {
margin-right: 16rpx;
padding: 6rpx 12rpx;
background: #e6f7ff;
border-radius: 8rpx;
}
.group-time-text {
font-size: 24rpx;
color: #1890ff;
font-weight: 500;
}
/* 单项时长显示 */
.all-time-display {
padding: 12rpx 16rpx;
background: #f6ffed;
border-radius: 8rpx;
border: 1rpx solid #b7eb8f;
}
.all-time-value {
font-size: 28rpx;
color: #52c41a;
font-weight: 600;
}
/* 添加计划按钮 */
.add-plan-btn {
display: flex;
@@ -521,6 +721,31 @@
}
}
/* 添加分组按钮 */
.add-group-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 28rpx 32rpx;
margin-top: 20rpx;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.2);
}
.add-group-text {
font-size: 30rpx;
color: #fff;
font-weight: 600;
}
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;

View File

@@ -0,0 +1,187 @@
<template>
<view class="contact-container">
<!-- 编辑表单 -->
<view class="edit-card">
<view class="card-header">
<u-icon name="edit-pen" size="20" color="#faad14"></u-icon>
<text class="card-title">修改联系方式</text>
</view>
<view class="form-content">
<view class="form-group">
<text class="form-label">联系人姓名</text>
<input class="form-input" v-model="kefuInfo.name" placeholder="请输入联系人姓名" type="text" />
</view>
<view class="form-group">
<text class="form-label">联系电话</text>
<input class="form-input" v-model="kefuInfo.phone" placeholder="请输入联系电话" type="number"
maxlength="11" />
</view>
<button class="save-btn" @click="saveContact">保存修改</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService'
import { onLoad } from '@dcloudio/uni-app'
let kefuInfo = ref<any>({})
onLoad(() => {
getData()
})
const getData = () => {
userService.GetKefuInfo().then(res => {
if (res.code == 0) {
kefuInfo.value = res.data.kefuInfo
} else {
Service.Msg(res.msg)
}
})
}
const saveContact = () => {
if (!kefuInfo.value.name.trim()) {
Service.Msg('请输入联系人姓名')
return
}
if (!kefuInfo.value.phone.trim()) {
Service.Msg('请输入联系电话')
return
}
const phoneReg = /^1[3-9]\d{9}$|^400-\d{3}-\d{4}$|^\d{3,4}-\d{7,8}$/
if (!phoneReg.test(kefuInfo.value.phone)) {
Service.Msg('请输入有效的联系电话')
return
}
userService.UpdateKefu(kefuInfo.value.phone, kefuInfo.value.name).then(res => {
if (res.code == 0) {
Service.Msg('修改成功!')
getData()
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.contact-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
.contact-card,
.edit-card {
background-color: #fff;
border-radius: 24rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 20rpx;
.card-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
}
.contact-info {
background-color: #f8f9fa;
border-radius: 16rpx;
padding: 24rpx;
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 28rpx;
color: #666;
}
.info-value {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
}
}
.form-content {
.form-group {
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.form-input {
width: 100%;
height: 88rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
transition: all 0.2s ease;
&:focus {
background-color: #fff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.15);
}
}
}
.save-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx;
border: none;
font-size: 32rpx;
color: #fff;
font-weight: 700;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.4);
transition: all 0.3s ease;
&:active {
transform: scale(0.97);
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
}
}
</style>

3872
src/pages/userFunc/cs.vue Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -33,10 +33,15 @@
</view>
</view>
<view class="project-stats">
<view class="stat-badge">
<view v-if="project.planType=='混氧项目'" class="stat-badge">
<u-icon name="account" size="14" color="#1890ff"></u-icon>
<text class="badge-text">{{ JSON.parse(project.project).length }}个计划</text>
</view>
<view v-else class="stat-badge">
<u-icon name="account" size="14" color="#1890ff"></u-icon>
<text class="badge-text">{{ project.users.length }}位学员</text>
</view>
</view>
</view>
@@ -90,7 +95,14 @@
// 查看项目详情
const viewProjectDetail = (project : any) => {
Service.Msg(`查看「${project.name}」详情`)
if(project.planType=='分段项目'){
Service.GoPage('/pages/userFunc/segmentation?id=' + project.planId)
}else if(project.planType=='计时项目'){
Service.GoPage('/pages/userFunc/swiming?id=' + project.planId)
}else{
Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId)
}
}
// 删除项目

View File

@@ -1,39 +1,35 @@
<template>
<view class="segmentation-container">
<!-- 配置区域 -->
<view class="config-section">
<view class="config-header">
<text class="config-title">分段设置</text>
<u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting" size="24"
color="#1890ff"></u-icon>
</view>
<view class="config-info">
<text class="info-text">总距离: {{ totalDistance }} ({{ segmentCount }} × {{ segmentDistance }})</text>
</view>
</view>
<!-- 总计时器区域 -->
<view class="total-time-section">
<view class="total-time-section" style="position: relative;">
<view class="timer-bar">
<view class="timer-time">{{ formatTime(currentTime) }}</view>
</view>
<view class="config-info">
<view class="info-text">总距离: {{ totalDistance }} ({{ segmentCount }} × {{ segmentDistance }})
{{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
</view>
<view class="" style="position: absolute; top: 20rpx; right: 20rpx; ">
<u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting"
size="24" color="#1890ff"></u-icon>
</view>
</view>
<!-- 学生列表 -->
<view class="students-section">
<view class="section-header">
<view class="students-section" style="margin-top: 20rpx;">
<!-- <view class="section-header">
<text class="section-title">学生列表</text>
<text class="student-count">{{ students.length }}位学生</text>
</view>
</view> -->
<view class="students-list">
<view v-for="(student, index) in students" :key="student.id" class="student-item"
:class="{ 'not-started': !student.hasStarted }">
<view class="student-header">
<view class="student-number" :class="{ 'started': student.hasStarted }">{{ student.number }}
<view class="student-number" @click="showStudentRecords(student)"
:class="{ 'started': student.hasStarted }">{{ student.number }}
</view>
<view class="student-info">
<text class="student-name">{{ student.name }}</text>
@@ -50,17 +46,14 @@
<view class="student-buttons">
<button class="record-btn" :disabled="!student.hasStarted"
@click="recordStudentSegment(student)">
<text class="btn-text">记录</text>
</button>
<button class="view-btn" @click="showStudentRecords(student)">
<text class="btn-text">详情</text>
</button>
<button class="reset-btn" @click="resetStudent(student)">
<view @click="resetStudent(student)" class="">
<u-icon name="reload" :bold="true" size="24" color="#1890ff"></u-icon>
</view>
<!-- <button class="reset-btn" @click="resetStudent(student)">
<text class="btn-text">重置</text>
</button>
</button> -->
</view>
</view>
</view>
@@ -105,7 +98,7 @@
</view>
</view>
<view class="record-list"
<scroll-view class="record-list" scroll-y
v-if="currentStudent && currentStudent.segments && currentStudent.segments.length > 0">
<view class="record-header">
<text class="record-cell header-cell">分段</text>
@@ -119,7 +112,7 @@
<text class="record-cell time-cell">{{ formatTime(segment.time) }}</text>
<text class="record-cell time-cell">{{ formatTimeDiff(index, segment.time) }}</text>
</view>
</view>
</scroll-view>
<view class="empty-record" v-else>
<u-icon name="info-circle" size="48" color="#ccc"></u-icon>
@@ -171,10 +164,10 @@
let planId = ref('')
onLoad((data : any) => {
planId.value = data.id
})
onShow(()=>{
onShow(() => {
getPlanInfo()
})
@@ -183,17 +176,17 @@
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
// planName.value = res.data.plan.name
segmentDistance.value=res.data.plan.subsectionDistance/res.data.plan.subsectionInt
segmentCount.value=res.data.plan.subsectionInt
segmentDistance.value = res.data.plan.subsectionDistance / res.data.plan.subsectionInt
segmentCount.value = res.data.plan.subsectionInt
// 将计划数据转换为选手数据
// athletes.value = res.data.plan.users.
students.value=res.data.plan.users.map((item:any,index:any)=>{
students.value = res.data.plan.users.map((item : any, index : any) => {
return {
id: item.studentId,
number: index+1,
number: index + 1,
name: item.name,
segments: [],
hasStarted: res.data.plan.departType == '间隔出发'?false:true,
hasStarted: res.data.plan.departType == '间隔出发' ? false : true,
startTime: 0
}
})
@@ -205,12 +198,12 @@
})
}
// 格式化时间显示
// 格式化时间显示(分:秒.毫秒)
const formatTime = (seconds : number) : string => {
const hours = Math.floor(seconds / 3600)
const mins = Math.floor((seconds % 3600) / 60)
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${millis.toString().padStart(2, '0')}`
}
// 格式化持续时间
@@ -230,10 +223,12 @@
return result
}
// 格式化时间差
// 格式化时间差(详情弹窗用)
// 第一段显示累计时间,后续段显示分段用时
const formatTimeDiff = (index : number, currentTime : number) : string => {
if (index === 0) {
return '00:00:00'
// 第一段显示累计时间
return formatTime(currentTime)
}
if (!currentStudent.value || !currentStudent.value.segments[index - 1]) {
return '00:00:00'
@@ -243,19 +238,27 @@
return formatTime(diff)
}
// 获取学生最后一次记录的累计时间
// 获取学生最后一次记录的分段用时
// 第一段返回累计时间,后续段返回当前段用时(当前累计 - 上一次累计)
const getLastSegmentTime = (student : any) : number => {
if (!student.segments || student.segments.length === 0) {
return 0
}
return student.segments[student.segments.length - 1].time
const lastIndex = student.segments.length - 1
if (lastIndex === 0) {
// 第一段,返回累计时间
return student.segments[0].time
}
// 后续段,返回分段用时
return student.segments[lastIndex].time - student.segments[lastIndex - 1].time
}
// 简化时间格式化(分:秒)
// 简化时间格式化(分:秒.毫秒
const formatSimpleTime = (seconds : number) : string => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${millis.toString().padStart(2, '0')}`
}
// 开始计时
@@ -268,7 +271,7 @@
timerInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000
currentTime.value = elapsed
}, 10)
}, 1)
// 间隔出发模式
if (startMode.value === 1) {
@@ -323,7 +326,6 @@
// 记录学生分段
const recordStudentSegment = (student : any) => {
console.log(22222);
if (!isRunning.value) {
Service.Msg('请先开始计时')
return
@@ -345,12 +347,19 @@
if (startMode.value === 1) {
recordTime = currentTime.value - student.startTime
}
student.segments.push({
time: recordTime
time: Number(recordTime).toFixed(3)
})
Service.Msg(`${student.name}${student.segments.length}段已记录`)
// 所有学生分段记录完成后自动停止计时
const allCompleted = students.value.every(s => s.segments.length >= maxSeg)
if (allCompleted) {
stopTimer()
Service.Msg('所有学生记录完成')
}
}
// 重置学生
@@ -367,8 +376,8 @@
currentTime.value = 0
students.value.forEach(student => {
student.segments = []
student.hasStarted = startMode.value == 1?false:true,
student.startTime = 0
student.hasStarted = startMode.value == 1 ? false : true,
student.startTime = 0
})
Service.Msg('已全部重置')
}
@@ -377,18 +386,57 @@
const saveData = () => {
// 检查是否有记录的数据
const hasData = students.value.some(s => s.segments.length > 0)
let isSave = students.value.every(s => s.segments.length >= segmentCount.value)
if (!hasData) {
Service.Msg('暂无数据可保存')
Service.Msg('暂无数据可保存!')
return
}
let data = [
{
studentId: "",
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
})
})
uni.showModal({
title: '提示', // 对话框标题
content: '请确定提交数据!', // 显示的内容
showCancel: true, // 是否显示取消按钮
cancelText: '取消', // 取消按钮的文字
confirmText: '确定', // 确认按钮的文字
success: function (res) {
if (res.confirm) {
PlanService.AddPlanLog(planId.value, '分段项目', '', JSON.stringify(data), '').then(res => {
if (res.code == 0) {
Service.Msg('提交成功!')
} else {
Service.Msg(res.msg)
}
})
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
// 这里可以添加保存到后端或本地存储的逻辑
Service.Msg('保存成功', 'success')
// 保存后可以选择返回上一页
setTimeout(() => {
Service.GoPageBack()
}, 1000)
}
// 显示学生记录弹窗
@@ -422,33 +470,21 @@
padding-bottom: 180rpx;
}
/* 配置区域 */
.config-section {
/* 总计时器区域 */
.total-time-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.config-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.config-info {
margin-top: 10rpx;
background-color: #e6f7ff;
border-radius: 8rpx;
padding: 16rpx 20rpx;
text-align: center;
.info-text {
font-size: 26rpx;
@@ -458,17 +494,8 @@
}
}
/* 总计时器区域 */
.total-time-section {
margin-top: 20rpx;
margin-bottom: 30rpx;
}
.timer-bar {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: center;
@@ -548,6 +575,7 @@
gap: 12rpx;
.student-name {
width: 120rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
@@ -566,7 +594,7 @@
}
.total-time-text {
font-size: 24rpx;
font-size: 30rpx;
color: #999;
font-family: 'DIN Alternate', monospace;
font-weight: 500;
@@ -601,7 +629,7 @@
.view-btn,
.reset-btn {
height: 56rpx;
padding: 0 20rpx;
padding: 0 35rpx;
border-radius: 8rpx;
border: none;
font-size: 24rpx;
@@ -643,7 +671,7 @@
.record-popup {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
max-height: 80vh;
height: 70vh;
overflow: hidden;
display: flex;
flex-direction: column;

View File

@@ -7,7 +7,7 @@
<view class="form-title">项目信息</view>
<view class="form-group">
<text class="form-label">项目名称</text>
<input class="form-input" v-model="courseData.projectName" placeholder="请输入项目名称"
<input class="form-input" :disabled="planId" v-model="courseData.projectName" placeholder="请输入项目名称"
placeholder-class="input-placeholder" />
</view>
</view>
@@ -115,33 +115,20 @@
@click="toggleStudentSelect(student.studentId)">
<view class="student-checkbox"
:class="{ checked: selectedStudentIds.includes(student.studentId) }">
<u-icon v-if="selectedStudentIds.includes(student.studentId)" name="checkmark" size="14"
color="#fff"></u-icon>
</view>
<view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
<text v-if="selectedStudentIds.includes(student.studentId)" class="checkbox-index">
{{ selectedStudentIds.indexOf(student.studentId) + 1 }}
</text>
</view>
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
</view>
</view>
</view>
<!-- 已选学生预览 -->
<view v-if="selectedStudents.length > 0" class="selected-preview">
<view class="preview-header">
<text class="preview-title">已选学生</text>
</view>
<view class="preview-list">
<view v-for="(student, index) in selectedStudents" :key="student.studentId" class="preview-item">
<text class="preview-index">{{ index + 1 }}</text>
<text class="preview-name">{{ student.name }}</text>
<view class="preview-remove" @click.stop="removeSelectedStudent(student.studentId)">
<u-icon name="close" size="14" color="#ff4d4f"></u-icon>
<view v-if="planId && type==1 " class="student-time" >
pb:{{ student.quicklyTime}}
</view>
</view>
</view>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 80rpx;">
@@ -233,7 +220,7 @@
const getStudentList = async () => {
loading.value = true
studentService.GetStudentList().then(res => {
studentService.GetStudentList(planId.value ).then(res => {
if (res.code == 0) {
allStudents.value = res.data
loading.value = false
@@ -326,10 +313,20 @@
PlanService.AddPlan(data).then(res=>{
if(res.code==0){
Service.Msg('添加成功!')
setTimeout(()=>{
Service.Msg( planId.value?'修改成功!': '添加成功!')
if(planId.value){
Service.GoPageBack()
},1000)
return
}
if(type.value=='1'){
Service.GoPageDelse('/pages/userFunc/swiming?id=' + res.data.planId)
}else{
Service.GoPageDelse('/pages/userFunc/segmentation?id=' + res.data.planId)
}
}else{
Service.Msg(res.msg)
}
@@ -697,14 +694,15 @@
/* 学生列表 */
.student-list {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 12rpx;
.student-item {
width: calc((100% - 24rpx) / 3);
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx;
gap: 8rpx;
padding: 16rpx 8rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
border: 2rpx solid transparent;
@@ -720,8 +718,8 @@
}
.student-checkbox {
width: 40rpx;
height: 40rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid #d9d9d9;
display: flex;
@@ -734,6 +732,13 @@
border-color: #1890ff;
background-color: #1890ff;
}
.checkbox-index {
font-size: 22rpx;
color: #fff;
font-weight: 600;
line-height: 1;
}
}
.student-avatar {
@@ -745,7 +750,7 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
.avatar-text {
color: #fff;
font-size: 28rpx;
@@ -754,14 +759,19 @@
}
.student-info {
flex: 1;
min-width: 0;
width: 100%;
.student-name {
font-size: 28rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
margin-bottom: 6rpx;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.student-time{
font-size: 24rpx;
color: #888;
display: block;
overflow: hidden;
text-overflow: ellipsis;
@@ -771,6 +781,7 @@
.student-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
.gender-badge {

View File

@@ -19,17 +19,19 @@
</view>
<view v-else class="students-list">
<view v-for="(student, index) in students" :key="student.id" class="student-card">
<view v-for="(student, index) in students" :key="student.id" class="student-card" >
<view class="student-info">
<view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<view class="student-details">
<view class="detail-row">
<text class="student-name">{{ student.name }}</text>
<view class="student-gender" :class="student.sex === '男' ? 'male' : 'female'">
<u-icon :name="student.sex === '男' ? 'man' : 'woman'" size="14"></u-icon>
{{ student.sex }}
<view class="detail-row" style="justify-content: space-between;">
<view style="display: flex; align-items: center; gap: 8rpx;">
<text class="student-name">{{ student.name }}</text>
<view class="student-gender" :class="student.sex === '男' ? 'male' : 'female'">
<u-icon :name="student.sex === '男' ? 'man' : 'woman'" size="14"></u-icon>
{{ student.sex }}
</view>
</view>
</view>
<view class="detail-row">
@@ -48,19 +50,23 @@
<text class="detail-label">住址</text>
<text class="detail-value address-text">{{ student.address }}</text>
</view>
<view class="detail-row">
<text class="detail-label">状态</text>
<up-switch v-model="student.active" size="16" @change="confirmDelete(student)" ></up-switch>
</view>
</view>
</view>
<view class="student-actions">
<view class="action-btn edit-btn" @click="openEditModal(student)">
<u-icon name="edit-pen" size="18" color="#1890ff"></u-icon>
</view>
</view>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 100rpx;" >
<view class="" style="width: 100%; height: 100rpx;">
</view>
<!-- 底部添加按钮 -->
<view class="bottom-add-btn">
@@ -87,15 +93,19 @@
<view class="form-group">
<text class="form-label">性别</text>
<view class="gender-selector">
<view class="gender-option" :class="{ active: formData.gender === '男' }" @click="formData.gender = '男'">
<view class="gender-option" :class="{ active: formData.gender === '男' }"
@click="formData.gender = '男'">
<view class="gender-icon-box">
<u-icon name="man" size="20" :color="formData.gender === '男' ? '#fff' : '#1890ff'"></u-icon>
<u-icon name="man" size="20"
:color="formData.gender === '男' ? '#fff' : '#1890ff'"></u-icon>
</view>
<text></text>
</view>
<view class="gender-option" :class="{ active: formData.gender === '女' }" @click="formData.gender = '女'">
<view class="gender-option" :class="{ active: formData.gender === '女' }"
@click="formData.gender = '女'">
<view class="gender-icon-box">
<u-icon name="woman" size="20" :color="formData.gender === '女' ? '#fff' : '#fa8c16'"></u-icon>
<u-icon name="woman" size="20"
:color="formData.gender === '女' ? '#fff' : '#fa8c16'"></u-icon>
</view>
<text></text>
</view>
@@ -148,13 +158,13 @@
import { studentService } from '@/Service/swimming/studentService'
import { ref } from "vue"
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 学员列表
const students = ref<Array<any>>([])
@@ -196,12 +206,12 @@
}
// 打开编辑弹窗
const openEditModal = (student :any) => {
const openEditModal = (student : any) => {
editingStudent.value = student
formData.value = {
id: student.studentId,
name: student.name,
gender: student.sex?student.sex: '男',
gender: student.sex ? student.sex : '男',
birthDate: student.birthday,
school: student.school,
address: student.address
@@ -241,7 +251,11 @@
status.value = 'loading'
studentService.GetStudentListPage(page.value.toString()).then(res => {
if (res.code == 0) {
students.value = [...students.value, ...res.data]
const list = res.data.map((item : any) => ({
...item,
active:item.status==1?true:false
}))
students.value = [...students.value, ...list]
status.value = res.data.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
@@ -264,19 +278,19 @@
// 构造接口请求数据
const requestData = {
studentId:formData.value.id,
studentId: formData.value.id ? formData.value.id : '',
name: formData.value.name,
sex: formData.value.gender,
birthday: formData.value.birthDate,
school: formData.value.school ,
address: formData.value.address
school: formData.value.school ? formData.value.school : '',
address: formData.value.address ? formData.value.address : ''
}
console.log(requestData);
// 调用添加学员接口
studentService.Add(requestData).then((content) => {
if (content.code == 0) {
Service.Msg('添加成功')
Service.Msg(formData.value.id ? '修改成功!' : '添加成功!')
getData()
} else {
Service.Msg(content.msg)
@@ -287,17 +301,9 @@
}
// 确认删除
const confirmDelete = (student : Student) => {
uni.showModal({
title: '确认删除',
content: `确定要删除学员「${student.name}」吗?`,
confirmColor: '#ff4d4f',
success: (res) => {
if (res.confirm) {
deleteStudent(student.id)
}
}
})
const confirmDelete = (student : any) => {
deleteStudent(student.studentId)
}
// 删除学员
@@ -305,7 +311,7 @@
studentService.Delete(id).then(res => {
if (res.code == 0) {
students.value = students.value.filter(s => s.id !== id)
Service.Msg('删除成功', 'success')
Service.Msg('修改成功!')
} else {
Service.Msg(res.msg)
}
@@ -329,9 +335,10 @@
margin-bottom: 24rpx;
.header-title {
display: flex;
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-size: 36rpx;
font-weight: 700;
@@ -501,10 +508,10 @@
}
&.delete-btn {
background-color: #fff1f0;
background-color: #f3f3f3;
&:active {
background-color: #ffccc7;
background-color: #f3f3f3;
transform: scale(0.92);
}
}

View File

@@ -11,17 +11,13 @@
</view>
<!-- 计划信息 -->
<view class="plan-info" v-if="planId">
<text class="plan-title">项目名称: {{ planName }}</text>
<text class="plan-title">{{ planName }} · {{ stopwatchMode=='interval'?'间隔触发':'一起出发' }} ·
{{ groupedAthletes.length }}{{ athletes.length }}位选手</text>
</view>
</view>
<!-- 选手列表 -->
<view class="athletes-section">
<view class="section-header">
<text class="section-title">间隔出发·{{ groupedAthletes.length }}</text>
<text class="athlete-count">{{ athletes.length }}位选手</text>
</view>
<view class="athletes-list">
<view v-for="group in groupedAthletes" :key="group.groupNumber" class="group-wrapper">
<view class="group-header">
@@ -41,9 +37,10 @@
<text class="athlete-name">{{ athlete.name }}</text>
</view>
<view class="time-wrapper">
<text class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text>
<text class="best-time">最快:
{{ athlete.bestTime !== null ? formatTime(athlete.quicklyTime) : '无' }}</text>
<text
class="athlete-time">{{ !athlete.time? ' 0000″00 ':formatTime(athlete.time) }}</text>
<text class="best-time">PB:
{{ athlete.studentQuicklyTime !== null ? athlete.studentQuicklyTime: '无' }}</text>
</view>
<view class="" style="display: flex;align-items: center; gap: 10rpx;">
@@ -58,30 +55,64 @@
</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" :safeAreaInsetBottom='false'
@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">{{ item.studentQuicklyTime }}</text>
</view>
</scroll-view>
</view>
</view>
</u-popup>
<!-- 可拖动悬浮球 -->
<view class="float-ball" @click="ballClick(isRunning)" :style="ballStyle" @touchstart="touchStart" @touchmove="touchMove"
@touchend="touchEnd">
<view class="ball-shine"></view>
<!-- @click="startTimer" @click="recordNextAthlete" -->
<view v-if="!isRunning" class="ball-text">开始</view>
<view v-if="isRunning" class="ball-text">记录</view>
</view>
<!-- 底部控制按钮 -->
<view class="control-buttons">
<view class="button-group">
<button v-if="!isRunning" class="start-btn" @click="startTimer">
<u-icon name="play-right" size="24" color="#fff"></u-icon>
<text class="btn-text">开始</text>
</button>
<button v-if="isRunning" class="record-btn" @click="recordNextAthlete">
<u-icon name="checkmark-circle" size="24" color="#fff"></u-icon>
<text class="btn-text">记录</text>
</button>
</view>
<view class="button-group">
<button v-if="!isRunning" class="reset-all-btn" @click="resetAll">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="btn-text">重置</text>
</button>
<button v-if="isRunning" class="reset-all-btn" @click="stopTimer">
<u-icon name="pause" size="24" color="#fff"></u-icon>
<text class="btn-text">暂停</text>
</button>
</view>
<button class="ctrl-btn btn-submit" @click="submitData">
<u-icon name="checkmark-circle" size="24" color="#fff"></u-icon>
<text class="btn-text">提交</text>
</button>
<button class="ctrl-btn btn-data" @click="Service.GoPage('/pages/dataAnalyze/timingAnalze')">
<u-icon name="list-dot" size="24" color="#fff"></u-icon>
<text class="btn-text">数据</text>
</button>
<button v-if="!isRunning" class="ctrl-btn btn-reset" @click="resetAll">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="btn-text">重置</text>
</button>
<button v-if="isRunning" class="ctrl-btn btn-pause" @click="stopTimer">
<text class="btn-text">暂停</text>
</button>
</view>
</view>
</template>
@@ -114,38 +145,77 @@
// 计划ID
const planId = ref('')
// 项目名称
let planName=ref('')
let planName = ref('')
// 选手列表
const athletes = ref<Array<any>>([])
// 分组设置
const groupSize = ref('0')
// 创建播放器
const video = uni.createInnerAudioContext()
// 计时器状态
const isRunning = ref(false)
const currentTime = ref(0)
let timerInterval : number | null = null
let startTime : number = 0
// 当前要记录的选手索引
const currentAthleteIndex = ref(0)
// 秒表模式
const stopwatchMode = ref<'interval' | 'together'>('interval')
// 间隔时间(秒)
const intervalTime = ref('0')
let record = ref<Array<any>>([])
let showRecord = ref(false)
// 已提示过出发的组号集合(间隔模式用)
const notifiedGroupNumbers = ref<Set<number>>(new Set())
// 可拖动悬浮球
const FLOAT_BALL_KEY = 'swiming_float_ball_position'
const ballPosition = ref({ x: 20, y: 300 })
const startPosition = ref({ x: 0, y: 0 })
const touchStartPoint = ref({ x: 0, y: 0 })
const ballSize = ref({ w: 50, h: 50 })
const ballStyle = computed(() => {
return `left:${ballPosition.value.x}px;top:${ballPosition.value.y}px;`
})
onLoad((data : any) => {
planId.value = data.id
const sys = uni.getSystemInfoSync()
ballSize.value.w = sys.windowWidth * 50 / 375
ballSize.value.h = ballSize.value.w
ballPosition.value = {
x: 20,
y: sys.windowHeight - ballSize.value.h - 200
}
const saved = uni.getStorageSync(FLOAT_BALL_KEY)
if (saved) {
try {
const pos = JSON.parse(saved)
if (typeof pos.x === 'number' && typeof pos.y === 'number') {
ballPosition.value = {
x: Math.max(0, Math.min(pos.x, sys.windowWidth - ballSize.value.w)),
y: Math.max(0, Math.min(pos.y, sys.windowHeight - ballSize.value.h))
}
}
} catch (e) { }
}
})
onShow(()=>{
onShow(() => {
getPlanInfo()
})
// 获取计划详情
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
@@ -153,15 +223,30 @@
planName.value = res.data.plan.name
// 将计划数据转换为选手数据
athletes.value = res.data.plan.users
groupSize.value=res.data.plan.groupInt
stopwatchMode.value=res.data.plan.departType=='间隔出发'?'interval':'together'
intervalTime.value=res.data.plan.interval
groupSize.value = res.data.plan.groupInt
stopwatchMode.value = res.data.plan.departType == '间隔出发' ? 'interval' : 'together'
intervalTime.value = res.data.plan.interval
} else {
Service.Msg(res.msg)
}
})
}
// 悬浮球事件
const ballClick=( data:any)=>{
if(data){
recordNextAthlete()
}else{
startTimer()
}
}
// 播报
const speak=()=>{
video.src='/static/icon/video.mp3'
video.play()
}
// 计算分组
const groupedAthletes = computed<Group[]>(() => {
const size = parseInt(groupSize.value) || 2
@@ -193,7 +278,7 @@
const index = athletes.value.findIndex(a => a.studentId === athlete.studentId)
const interval = parseFloat(intervalTime.value) || 10
const size = parseInt(groupSize.value) || 2
// 根据分组计算偏移第n组偏移 (n-1) * interval
const groupNumber = Math.floor(index / size)
return groupNumber * interval
@@ -209,7 +294,7 @@
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
const ms = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${ms.toString().padStart(2, '0')}`
}
// 开始计时
@@ -217,12 +302,26 @@
if (isRunning.value) return
isRunning.value = true
notifiedGroupNumbers.value.clear()
startTime = Date.now() - currentTime.value * 1000
timerInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000
currentTime.value = elapsed
// 间隔模式下,检查是否有新组到达出发时间
if (stopwatchMode.value === 'interval') {
const interval = parseFloat(intervalTime.value) || 10
groupedAthletes.value.forEach(group => {
const groupOffset = (group.groupNumber - 1) * interval
if (elapsed >= groupOffset && !notifiedGroupNumbers.value.has(group.groupNumber)) {
notifiedGroupNumbers.value.add(group.groupNumber)
// Service.Msg(`第${group.groupNumber}组出发了`)
speak()
}
})
}
// 更新未完成选手的时间(考虑偏移量)
athletes.value.forEach(athlete => {
if (!athlete.finished) {
@@ -230,7 +329,7 @@
athlete.time = Math.max(0, elapsed - offset)
}
})
}, 10)
}, 30)
}
// 停止计时
@@ -341,6 +440,7 @@
stopTimer()
currentTime.value = 0
currentAthleteIndex.value = 0
notifiedGroupNumbers.value.clear()
athletes.value.forEach(athlete => {
athlete.time = 0
@@ -349,7 +449,103 @@
})
})
}
// 处理数据
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 = () => {
const hasData = athletes.value.some(a => a.time > 0)
const allFinished = athletes.value.every(a => a.finished)
if (!hasData) {
Service.Msg('暂无数据可提交')
return
}
stopTimer()
let data = [{
planName: "",
studentId: "",
studentName: "",
time: ""
}]
data = []
athletes.value.map((item : any) => {
data.push({
planName: planName.value,
studentId: item.studentId,
studentName: item.name,
time: item.time
})
})
uni.showModal({
title: '提示', // 对话框标题
content: '请确定提交数据!', // 显示的内容
showCancel: true, // 是否显示取消按钮
cancelText: '取消', // 取消按钮的文字
confirmText: '确定', // 确认按钮的文字
success: function (res) {
if (res.confirm) {
PlanService.AddPlanLog(planId.value, '计时项目', JSON.stringify(data), '', '', '').then(res => {
if (res.code == 0) {
Service.Msg('提交成功!')
stopTimer()
currentTime.value = 0
currentAthleteIndex.value = 0
athletes.value.forEach(athlete => {
athlete.time = 0
athlete.finished = false
athlete.startOffset = undefined
})
if (res.data.record.length > 0) {
record.value = res.data.record
showRecord.value = true
}
} else {
Service.Msg(res.msg)
}
})
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}
// 悬浮球拖动事件
const touchStart = (e : any) => {
const touch = e.touches[0]
touchStartPoint.value = { x: touch.clientX, y: touch.clientY }
startPosition.value = { ...ballPosition.value }
}
const touchMove = (e : any) => {
const touch = e.touches[0]
const deltaX = touch.clientX - touchStartPoint.value.x
const deltaY = touch.clientY - touchStartPoint.value.y
ballPosition.value = {
x: startPosition.value.x + deltaX,
y: startPosition.value.y + deltaY
}
}
const touchEnd = () => {
const sys = uni.getSystemInfoSync()
ballPosition.value = {
x: Math.max(0, Math.min(ballPosition.value.x, sys.windowWidth - ballSize.value.w)),
y: Math.max(0, Math.min(ballPosition.value.y, sys.windowHeight - ballSize.value.h))
}
uni.setStorageSync(FLOAT_BALL_KEY, JSON.stringify(ballPosition.value))
}
// 页面卸载时清理计时器
onUnmounted(() => {
@@ -384,6 +580,10 @@
font-size: 80rpx;
font-weight: bold;
color: #ff0000;
font-family: 'DIN Alternate', monospace;
min-width: 400rpx;
display: inline-block;
text-align: center;
}
.plan-info {
@@ -403,7 +603,7 @@
/* 选手列表区域 */
.athletes-section {
margin-top: 20rpx;
margin-top: 80rpx;
margin-bottom: 120rpx;
.section-header {
@@ -439,7 +639,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 28rpx;
padding: 10rpx 28rpx;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
border-left: 6rpx solid #1890ff;
@@ -472,12 +672,11 @@
.athlete-item {
display: flex;
align-items: center;
padding: 24rpx;
margin-bottom: 12rpx;
padding: 14rpx 24rpx;
background-color: #ffffff;
border-radius: 16rpx;
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03);
transition: all 0.3s;
transition: background-color 0.3s, border-color 0.3s;
border: 2rpx solid transparent;
&.finished {
@@ -586,137 +785,222 @@
}
}
/* 右下角悬浮提交按钮 */
.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 {
display: flex;
gap: 24rpx;
justify-content: center;
gap: 20rpx;
justify-content: space-between;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%);
padding: 32rpx 40rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08);
.button-group {
.ctrl-btn {
flex: 1;
display: flex;
justify-content: center;
}
.back-btn,
.start-btn,
.pause-btn,
.reset-all-btn,
.record-btn {
flex: 1;
height: 100rpx;
border-radius: 20rpx;
height: 92rpx;
border-radius: 24rpx;
border: none;
font-size: 32rpx;
font-size: 30rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%);
border-radius: 20rpx 20rpx 0 0;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rpx;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%);
}
:deep(.u-icon) {
position: relative;
z-index: 1;
}
}
.start-btn {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4);
gap: 10rpx;
transition: all 0.25s ease;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
}
.pause-btn {
background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(250, 84, 28, 0.4);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(250, 84, 28, 0.3);
}
.btn-submit {
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(250, 173, 20, 0.35);
}
.reset-all-btn {
.btn-data {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(24, 144, 255, 0.4);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3);
}
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(24, 144, 255, 0.35);
}
.back-btn {
background: linear-gradient(135deg, #722ed1 0%, #9254de 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(114, 46, 209, 0.4);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(114, 46, 209, 0.3);
}
}
.record-btn {
.btn-reset {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4);
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(82, 196, 26, 0.35);
}
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
.btn-pause {
background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%);
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(250, 84, 28, 0.35);
}
.btn-text {
font-size: 32rpx;
font-size: 30rpx;
color: #ffffff;
font-weight: 600;
letter-spacing: 2rpx;
}
}
/* 可拖动悬浮球 */
.float-ball {
position: fixed;
width: 170rpx;
height: 170rpx;
border-radius: 50%;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 8rpx 24rpx rgba(82, 196, 26, 0.45),
0 16rpx 40rpx rgba(0, 0, 0, 0.15),
inset 0 4rpx 10rpx rgba(255, 255, 255, 0.35);
z-index: 999;
touch-action: none;
transition: transform 0.2s ease;
&:active {
transform: scale(0.92);
}
.ball-shine {
position: absolute;
top: 20rpx;
left: 24rpx;
width: 36rpx;
height: 18rpx;
background: rgba(255, 255, 255, 0.4);
border-radius: 50%;
filter: blur(2rpx);
pointer-events: none;
}
.ball-text {
color: #fff;
font-size: 40rpx;
font-weight: 700;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.15);
position: relative;
z-index: 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>

View File

@@ -0,0 +1,288 @@
<template>
<view class="permission-container">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-box">
<u-icon name="search" size="20" color="#bbb"></u-icon>
<input class="search-input" v-model="keyword" placeholder="搜索用户姓名或手机号" confirm-type="search"
@input="onSearch" />
<view v-if="keyword" class="search-clear" @click="clearSearch">
<u-icon name="close-circle" size="18" color="#ccc"></u-icon>
</view>
</view>
</view>
<!-- 用户列表 -->
<view class="users-section">
<view v-if="users.length === 0" class="empty-state">
<view class="empty-icon">
<u-icon name="account" size="64" color="#ddd"></u-icon>
</view>
<text class="empty-text">暂无用户</text>
</view>
<view v-else class="users-list">
<view v-for="user in users" :key="user.userId" class="user-card">
<view class="user-avatar">
<image v-if="user.headImg" class="avatar-img" :src="Service.GetMateUrlByImg(user.headImg)"
mode="aspectFill"></image>
<view v-else class="avatar-default">
<text class="avatar-text">{{ user.name ? user.name.charAt(0) : '?' }}</text>
</view>
</view>
<view class="user-info">
<text class="user-name">{{ user.name }}</text>
<text class="user-phone">{{ user.phone }}</text>
</view>
<view class="user-action">
<!-- 手写开关器status == 1 为开 -->
<view
class="custom-switch"
:class="{ 'switch-on': user.status == 1 }"
@click="toggleStatus(user)"
>
<view class="switch-thumb"></view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
const keyword = ref('')
const users = ref<Array<any>>([])
let page = ref(1)
let loadStatus = ref('nomore')
onLoad(() => {
getData()
})
onReachBottom(() => {
getList()
})
const getData = () => {
users.value = []
page.value = 1
loadStatus.value = 'loadmore'
getList()
}
const getList = () => {
if (loadStatus.value == 'loading' || loadStatus.value == 'nomore') {
return
}
loadStatus.value = 'loading'
userService.GetUserList(keyword.value, page.value).then((res: any) => {
if (res.code == 0) {
users.value = [...users.value, ...res.data.list]
loadStatus.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
const onSearch = () => {
getData()
}
const clearSearch = () => {
keyword.value = ''
getData()
}
const toggleStatus = (user: any) => {
userService.UpdateUserRole(user.userId).then(res=>{
if (res.code == 0) {
const newStatus = user.status == 1 ? 0 : 1
user.status = newStatus
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.permission-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
/* 搜索栏 */
.search-section {
margin-bottom: 20rpx;
.search-box {
display: flex;
align-items: center;
gap: 12rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 16rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
height: 48rpx;
}
.search-clear {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
}
}
}
/* 用户列表 */
.users-section {
.empty-state {
background-color: #fff;
border-radius: 24rpx;
padding: 80rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.empty-icon {
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
font-size: 30rpx;
font-weight: 600;
color: #666;
}
}
}
.users-list {
.user-card {
background-color: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-default {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.avatar-text {
color: #fff;
font-size: 30rpx;
font-weight: 700;
}
}
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
min-width: 0;
.user-name {
font-size: 32rpx;
font-weight: 700;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-phone {
font-size: 24rpx;
color: #999;
}
}
.user-action {
flex-shrink: 0;
}
}
}
/* 手写开关器 */
.custom-switch {
width: 96rpx;
height: 52rpx;
border-radius: 26rpx;
background-color: #d9d9d9;
position: relative;
transition: background-color 0.3s ease;
cursor: pointer;
.switch-thumb {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background-color: #fff;
position: absolute;
top: 4rpx;
left: 4rpx;
transition: transform 0.3s ease;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
&.switch-on {
background-color: #52c41a;
.switch-thumb {
transform: translateX(44rpx);
}
}
&:active {
opacity: 0.8;
}
}
</style>

BIN
src/static/icon/video.mp3 Normal file

Binary file not shown.

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

@@ -7,10 +7,12 @@
"合并单元格",
"表格",
"多级表头"
],
],
"repository": "",
"engines": {
"HBuilderX": "^4.36"
"HBuilderX": "^4.36",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
@@ -30,55 +32,67 @@
"data": "无",
"permissions": "无"
},
"npmurl": ""
"npmurl": "",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
"tcb": "",
"aliyun": "",
"alipay": ""
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
"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": "-"
}
},
"App": {
"app-vue": "y",
"app-nvue": "u",
"app-uvue": "u",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}

View File

@@ -10,15 +10,17 @@
"",
"打卡",
"日历选择"
],
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
"HBuilderX": "",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
@@ -36,51 +38,69 @@
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
"type": "component-vue",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
"tcb": "",
"aliyun": "",
"alipay": "x"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
"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": "-"
}
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}
}

View File

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