Compare commits

...

10 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
Ls
c9aeba0949 保存数据 2026-04-11 17:47:28 +08:00
Ls
f682c0316b 提交数据 2026-04-06 09:31:25 +08:00
Ls
a4bcd5ae1f '提交' 2026-03-31 08:40:11 +08:00
Ls
b8c05d23ae 页面修改 2026-03-28 08:55:48 +08:00
Ls
deaf39e943 最新状态 2026-03-26 08:34:32 +08:00
120 changed files with 43905 additions and 685 deletions

212
AGENTS.md Normal file
View File

@@ -0,0 +1,212 @@
# AGENTS.md - Swimming uni-app Project
## 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.
## 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 install
```
## Build And Dev Commands
### Common Development
```bash
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
```
### Common Build
```bash
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
```
### 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/
├── 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
```
## 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.
## 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()`.
## 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.
## 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`.
## 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.
## 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`.
## 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.
## 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.
## 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.
## 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.
## 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.

88
CLAUDE.md Normal file
View File

@@ -0,0 +1,88 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a uni-app Vue 3 + TypeScript swimming timer/training application called "智能秒表" (Smart Stopwatch). It supports multiple platforms including H5, WeChat mini-program, and native apps.
## Development Commands
```bash
# Development
npm run dev:h5 # Run H5 development server
npm run dev:mp-weixin # Run WeChat mini-program development
npm run dev:app # Run app development
# Build
npm run build:h5 # Build for H5
npm run build:mp-weixin # Build for WeChat mini-program
npm run build:app # Build for app
# Type checking
npm run type-check # Run TypeScript type checking
```
## Architecture
### Directory Structure
```
src/
├── pages/ # Main pages (index/, user/)
├── pages/userFunc/ # Sub-package for user features (timing, segmentation, project)
├── components/ # Reusable Vue components
├── Service/ # API service layer
│ ├── Service.ts # Main service class (extends BaseConfig)
│ └── BaseConfig.ts # Base configuration
├── common/
│ ├── Domain/ # TypeScript interfaces (Project, StudentRecord, TimingState)
│ └── Unit/ # Utility classes (HttpRequest, StoreAssist, UploadAssist)
├── static/ # Static assets
├── types/ # Additional TypeScript types
└── colorui/ # ColorUI CSS framework
```
### Service Layer Pattern
- `Service` class extends `BaseConfig` and provides:
- API requests: `Service.Request(url, method, data)` - handles 401 token expiration automatically
- Navigation: `Service.GoPage()`, `Service.GoPageTab()`, `Service.GoPageBack()`, `Service.GoPageDelse()`
- Storage: `Service.SetStorageCache()`, `Service.GetStorageCache()`, `Service.DelStorageCache()`
- Messages: `Service.Msg()`, `Service.Alert()`
- Loading: `Service.LoadIng()`, `Service.LoadClose()`
- File uploads: `Service.UpLoadMedia()`, `Service.uploadH5()`
### Pages Configuration
- Pages registered in `src/pages.json`
- User features in `subPackages` under `pages/userFunc/`
- Tab bar with 2 tabs: "项目" (index) and "我的" (user)
- Navigation style is custom for some pages (e.g., swimim page)
### Key Domain Types
- `Project`: Swimming project data (id, name, createdAt)
- `StudentRecord`: Student timing records
- `TimingState`: Timer state management
- `ResultData`: Standard API response wrapper
## Code Style
- Use tabs for indentation
- Single quotes for strings
- PascalCase for components and classes
- camelCase for variables and functions
- kebab-case for CSS classes
- Use `@/` path alias for imports from src/ directory
- Import Vue composition API from 'vue', uni-app hooks from `@dcloudio/uni-app`
## UI Framework
- **uview-plus** as primary UI component library (configured with easycom)
- **ColorUI** for CSS framework and icons
- Component prefix: `u-` (e.g., `<u-icon>`, `<u-button>`)
- Use `rpx` units for responsive design
## Important Notes
- Automatic 401 token handling: API requests through `Service.Request()` redirect to login on token expiration
- Clean up intervals in `onUnmounted()` lifecycle hook
- No test framework currently configured
- Platform-specific APIs prefixed with `uni.` (uni-app framework)

16
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,15 +12682,13 @@
}
},
"node_modules/uview-plus": {
"version": "3.3.54",
"resolved": "https://registry.npmmirror.com/uview-plus/-/uview-plus-3.3.54.tgz",
"integrity": "sha512-c/KcwTkbJed6ZZqxh7mreDFjtkq5ebNkMHsvgFn53xKVEbjWuGJ/zz4jOXWoO+n0r5hxH1LzrDXXDDiBDvqORA==",
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.3"
},
"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"
"HBuilderX": "^3.1.0",
"uni-app": "^4.66",
"uni-app-x": ""
}
},
"node_modules/v8-to-istanbul": {

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { NvpMerchService } from '@/Service/Nvp/NvpMerchService'
onLaunch(() => {
uni.hideTabBar()
getUpData()
console.log("App Launch");
});
onShow(() => {
@@ -12,39 +10,16 @@
onHide(() => {
console.log("App Hide");
});
const getUpData=()=>{
// #ifdef APP
plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => {
NvpMerchService.GetAppVersion().then(res=>{
console.log('wgtinfo.versionCode',wgtinfo.versionCode);
if (res.data.version > wgtinfo.versionCode) {
setTimeout(function() {
uni.navigateTo({
url: "/pages/upData/upData?info=" +
encodeURIComponent(
JSON.stringify(res.data))
})
}, 1000)
}
})
})
// #endif
}
</script>
<style lang="scss">
@import "uview-plus/index.scss";
@import "colorui/main.css";
@import "colorui/icon.css";
page {
--nav-mian: #F84F28; //全局颜色
--nav-vice: #F59D77; //副颜色
--nav-diluted: #F2C0A3; //淡颜色
}
@import "colorui/icon.css";
</style>
page {
--nav-mian: #22a6f2; //全局颜色
--nav-vice: #00a2ff; //副颜色
--nav-diluted: #6ba9cd; //淡颜色
}
</style>

View File

@@ -1,9 +1,11 @@
export class BaseConfig {
// protected static servesUrl: string = "http://192.168.0.190:8806";//线下
// 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://pay.xypays.cn";
protected static payuploadUrl: string = "https://swimming.api.xypays.cn";
}

View File

@@ -1,36 +0,0 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpAddressService {
private static GetAddressListPath: string = '/Address/GetAddressList';
/*****收货地址*****/
static GetAddressList(page: number) {
var result = Service.Request(this.GetAddressListPath, "GET", { page });
return result;
}
private static DelUserAddressPath: string = '/Address/DelUserAddress';
/*****删除地址*****/
static DelUserAddress(uaId: string) {
var result = Service.Request(this.DelUserAddressPath, "POST", { uaId });
return result;
}
private static AddUserAddressPath: string = '/Address/AddUserAddress';
/*****添加地址*****/
static AddUserAddress(uaId: string, name: string, phone: string, province: string, city: string, county: string, address: string, isDefault: number) {
var result = Service.Request(this.AddUserAddressPath, "POST", { uaId, name, phone, province, city, county, address, isDefault });
return result;
}
private static UpdateUserAddressPath: string = '/Address/UpdateUserAddress';
/*****修改默认地址*****/
static UpdateUserAddress(uaId: string) {
var result = Service.Request(this.UpdateUserAddressPath, "POST", { uaId });
return result;
}
}
export {
Service,
NvpAddressService
}

View File

@@ -1,43 +0,0 @@
import { Service } from '@/Service/Service';
/*****代理端接口*****/
class NvpAgentService {
private static LoginPath: string = '/Agent/Login';
/*****登录接口*****/
static Login(name: string, pwd: string) {
var result = Service.Request(this.LoginPath, "POST", { name, pwd });
return result;
}
private static GetAgentAccInfoPath: string = '/Agent/GetAgentAccInfo';
/*****账户接口*****/
static GetAgentAccInfo() {
var result = Service.Request(this.GetAgentAccInfoPath, "GET", "");
return result;
}
private static GetAgentAccLogPath: string = '/Agent/GetAgentAccLog';
/*****账户记录*****/
static GetAgentAccLog(code: string, page: number) {
var result = Service.Request(this.GetAgentAccLogPath, "GET", { code, page });
return result;
}
private static GetAgentMerchPath: string = '/Agent/GetAgentMerch';
/*****获取代理开通商家*****/
static GetAgentMerch(type: number, page: number) {
var result = Service.Request(this.GetAgentMerchPath, "GET", { type, page });
return result;
}
private static GetAgentHomePath: string = '/Agent/GetAgentHome';
/*****代理主页*****/
static GetAgentHome() {
var result = Service.Request(this.GetAgentHomePath, "GET", { });
return result;
}
}
export {
Service,
NvpAgentService
}

View File

@@ -1,156 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpApplyService {
// private static WithDrawPath: string = '/With/WithDraw';
// /*****佣金提现*****/
// static WithDraw(money: number, name: string, account: string) {
// var result = Service.Request(this.WithDrawPath, "POST", { money, name, account });
// return result;
// }
private static GetSiteMccCodeListPath: string = '/Apply/GetSiteMccCodeList';
/*****获取mcc列表*****/
static GetSiteMccCodeList(mercType:string, mchType: string) {
var result = Service.Request(this.GetSiteMccCodeListPath, "GET", {mercType, mchType });
return result;
}
private static GetBankTypeListPath: string = '/Apply/GetBankTypeList';
/*****获取银行列表*****/
static GetBankTypeList(name:string) {
var result = Service.Request(this.GetBankTypeListPath, "GET", {name});
return result;
}
private static GetAreaListPath: string = '/Apply/GetAreaList';
/*****获取银行地区列表*****/
static GetAreaList(areaCode:string) {
var result = Service.Request(this.GetAreaListPath, "GET", { areaCode});
return result;
}
private static GetBankCodeListPath: string = '/Apply/GetBankCodeList';
/*****获取银行代码列表*****/
static GetBankCodeList(bankType:string,cityCode:string,name:string) {
var result = Service.Request(this.GetBankCodeListPath, "GET", {bankType,cityCode,name});
return result;
}
private static SendApplyMerchPath: string = '/Apply/SendApplyMerch';
/*****进价提交*****/
static SendApplyMerch(para:any) {
var result = Service.Request(this.SendApplyMerchPath, "POST", para);
return result;
}
private static GetAgentMerchLogPath: string = '/Apply/GetAgentMerchLog';
/*****获取待审核*****/
static GetAgentMerchLog() {
var result = Service.Request(this.GetAgentMerchLogPath, "GET", {});
return result;
}
private static AuditApplyPath: string = '/Apply/AuditApply';
/*****确认资料*****/
static AuditApply(outId:string) {
var result = Service.Request(this.AuditApplyPath, "POST", {outId});
return result;
}
private static SetPayFeePath: string = '/Apply/SetPayFee';
/*****确认资料*****/
static SetPayFee(outId:string) {
var result = Service.Request(this.SetPayFeePath, "POST", {outId});
return result;
}
private static GetAssortListPath: string = '/Apply/GetAssortList';
/*****获取v派分类*****/
static GetAssortList(code:string) {
var result = Service.Request(this.GetAssortListPath, "GET", {code,parent:'0'});
return result;
}
private static GetAgentMerchPath: string = '/Agent/GetAgentMerch';
/*****获取已开通商家*****/
static GetAgentMerch(type:number,page:number) {
var result = Service.Request(this.GetAgentMerchPath, "GET", {type,page});
return result;
}
private static GetAppMerchInfoPath: string = '/Apply/GetAppMerchInfo';
/*****获取银盛已填写信息*****/
static GetAppMerchInfo(outId:string) {
var result = Service.Request(this.GetAppMerchInfoPath, "GET", {outId});
return result;
}
private static UpdateMerchPath: string = '/Apply/UpdateMerch';
/*****修改银盛信息*****/
static UpdateMerch(obj:any) {
var result = Service.Request(this.UpdateMerchPath, "POST", obj);
return result;
}
private static UploadImgPath: string = '/Apply/UploadImg';
/*****修改银盛图片*****/
static UploadImg(outId:string,picType:string,img:string) {
var result = Service.Request(this.UploadImgPath, "POST", {outId,picType,img});
return result;
}
private static GetAgentApplyPath: string = '/Apply/GetAgentApply';
/*****获取添加的商家*****/
static GetAgentApply(serch:string,page:number) {
var result = Service.Request(this.GetAgentApplyPath, "GET", {serch,page});
return result;
}
private static GetCategoryPath: string = '/Apply/GetCategory';
/*****获取商家类型*****/
static GetCategory() {
var result = Service.Request(this.GetCategoryPath, "GET", {});
return result;
}
private static AddMerchInfoPath: string = '/Apply/AddMerchInfo';
/*****添加商户*****/
static AddMerchInfo(obj:any) {
var result = Service.Request(this.AddMerchInfoPath, "POST", obj);
return result;
}
private static BandAppIdPath: string = '/Agent/BandAppId';
/*****绑定appid*****/
static BandAppId(merchId:string) {
var result = Service.Request(this.BandAppIdPath, "POST", {merchId});
return result;
}
}
export {
Service,
NvpApplyService
}

View File

@@ -1,50 +0,0 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpBankService {
private static GetPageBankListPath: string = '/Bank/GetPageBankList';
/*****用户银行卡列表*****/
static GetPageBankList(page: number) {
var result = Service.Request(this.GetPageBankListPath, "GET", { page });
return result;
}
private static GetBankListPath: string = '/Bank/GetBankList';
/*****用户银行卡列表*****/
static GetBankList() {
var result = Service.Request(this.GetBankListPath, "GET", "");
return result;
}
private static GetUnitBankListPath: string = '/Bank/GetUnitBankList';
/*****银行卡列表*****/
static GetUnitBankList() {
var result = Service.Request(this.GetUnitBankListPath, "GET", "");
return result;
}
private static AddUserBankPath: string = '/Bank/AddUserBank';
/*****添加银行卡*****/
static AddUserBank(account: string, bank: string, name: string) {
var result = Service.Request(this.AddUserBankPath, "POST", { account, bank, name });
return result;
}
private static UpdateBankPath: string = '/Bank/UpdateBank';
/*****修改银行卡*****/
static UpdateBank(ubId: string, bank: string, name: string, account: string) {
var result = Service.Request(this.UpdateBankPath, "POST", { ubId, bank, name, account });
return result;
}
private static DelUserBankPath: string = '/Bank/DelUserBank';
/*****删除银行卡*****/
static DelUserBank(ubId: string) {
var result = Service.Request(this.DelUserBankPath, "POST", { ubId });
return result;
}
}
export {
Service,
NvpBankService
}

View File

@@ -1,36 +0,0 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpLoginService {
private static GetWxLoginOpenIdPath: string = '/Login/GetWxLoginOpenId';
/*****小程序根据code获取openId*****/
static GetWxLoginOpenId(code: string, type: number) {
var result = Service.Request(this.GetWxLoginOpenIdPath, "GET", { code, type });
return result;
}
private static AppletLoginPath: string = '/Login/AppletLogin';
/*****普通登陆接口(小程序)*****/
static AppletLogin(openId: string, channel: string) {
var result = Service.Request(this.AppletLoginPath, "POST", { openId, channel });
return result;
}
private static RegistPath: string = '/Login/Regist';
/*****注册接口*****/
static Regist(remNo: string, openId: string, uniopenId: string, channel: string, type: number) {
var result = Service.Request(this.RegistPath, "POST", { remNo, openId, uniopenId, channel, type });
return result;
}
private static GetNumberPhonePath: string = '/Login/GetNumberPhone';
/*****获取手机号*****/
static GetNumberPhone(code: string) {
var result = Service.Request(this.GetNumberPhonePath, "GET", { code });
return result;
}
}
export {
Service,
NvpLoginService
}

View File

@@ -1,32 +0,0 @@
import { Service } from '@/Service/Service';
/*****设备接口*****/
class NvpMachineService {
private static GetMerchListPath: string = '/Machine/GetMerchList';
/*****商户设备*****/
static GetMerchList(merchId: string) {
var result = Service.Request(this.GetMerchListPath, "GET", { merchId });
return result;
}
private static DelMerchMachinePath: string = '/Machine/DelMerchMachine';
/*****删除设备*****/
static DelMerchMachine(merchId: string, machineId: string) {
var result = Service.Request(this.DelMerchMachinePath, "POST", { merchId, machineId });
return result;
}
private static AddMachinePath: string = '/Machine/AddMachine';
/*****添加设备*****/
static AddMachine(merchId: string, payCode: string) {
var result = Service.Request(this.AddMachinePath, "POST", { merchId, payCode });
return result;
}
}
export {
Service,
NvpMachineService
}

View File

@@ -1,106 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpMerchService {
private static GetMerchInfoPath: string = '/Merch/GetMerchInfo';
/*****获取商家信息*****/
static GetMerchInfo(merchId: string,lon:number,lat:number) {
var result = Service.Request(this.GetMerchInfoPath, "GET", { merchId,lon,lat});
return result;
}
private static GetMerchOrderPath: string = '/Merch/GetMerchOrder';
/*****获取商家营业数据*****/
static GetMerchOrder(merchId: string) {
var result = Service.Request(this.GetMerchOrderPath, "GET", { merchId });
return result;
}
private static UpdateMerchInfoPath: string = '/Merch/UpdateMerchInfo';
/*****修改商家信息*****/
static UpdateMerchInfo(merchId: string, logo: string ,name:string, phone:string,province:string,city:string,county:string,address:string,lon:string,lat:string,sTime:string,eTime:string,showImg:string) {
var result = Service.Request(this.UpdateMerchInfoPath, "POST", { merchId, logo,name ,phone,province,city,county,address,lon,lat,sTime,eTime,showImg});
return result;
}
private static GetMerchAccPath: string = '/Merch/GetMerchAcc';
/*****获取商家佣金数据*****/
static GetMerchAcc(merchId: string) {
var result = Service.Request(this.GetMerchAccPath, "GET", { merchId });
return result;
}
private static GetMerchAccLogPath: string = '/Merch/GetMerchAccLog';
/*****获取商家佣金记录*****/
static GetMerchAccLog(merchId: string,op:string,page:number) {
var result = Service.Request(this.GetMerchAccLogPath, "GET", { merchId,op,page });
return result;
}
private static MerchWithDrawPath: string = '/Merch/MerchWithDraw';
/*****商家佣金提现*****/
static MerchWithDraw(merchId: string, money: number ,name:string, account:string) {
var result = Service.Request(this.MerchWithDrawPath, "POST", { merchId, money,name,account});
return result;
}
private static GetMerchWithLogPath: string = '/Merch/GetMerchWithLog';
/*****商家佣金提现记录*****/
static GetMerchWithLog(merchId: string) {
var result = Service.Request(this.GetMerchWithLogPath, "GET", { merchId });
return result;
}
private static GetMerchTicketListPath: string = '/Merch/GetMerchTicketList';
/*****商家优惠券列表*****/
static GetMerchTicketList(merchId: string) {
var result = Service.Request(this.GetMerchTicketListPath, "GET", { merchId });
return result;
}
private static AddMerchTicketPath: string = '/Merch/AddMerchTicket';
/*****商家优惠券修改添加*****/
static AddMerchTicket(ticketId: string, merchId: string ,code:string, atkAcc:number,redAcc:number,count:number,useTime:Number,state:Number,addTime:string,endTime:string) {
var result = Service.Request(this.AddMerchTicketPath, "POST", { ticketId, merchId,code,atkAcc,redAcc,count,useTime,state,addTime,endTime});
return result;
}
private static DelMerchTicketPath: string = '/Merch/DelMerchTicket';
/*****商家删除优惠券*****/
static DelMerchTicket(ticketId: string) {
var result = Service.Request(this.DelMerchTicketPath, "POST", { ticketId});
return result;
}
private static GetTicketInfoPath: string = '/Merch/GetTicketInfo';
/*****获取优惠券详情*****/
static GetTicketInfo(ticketId: string) {
var result = Service.Request(this.GetTicketInfoPath, "GET", { ticketId });
return result;
}
private static GetAppVersionPath: string = '/Login/GetAppVersion';
/*****更新*****/
static GetAppVersion() {
var result = Service.Request(this.GetAppVersionPath, "GET", {type:2});
return result;
}
}
export {
Service,
NvpMerchService
}

View File

@@ -1,43 +0,0 @@
import { Service } from '@/Service/Service';
/*****公共接口*****/
class NvpPubService {
private static GetIndexPath: string = '/Pub/GetIndex';
/*****主页信息*****/
static GetIndex() {
var result = Service.Request(this.GetIndexPath, "GET", "");
return result;
}
private static GetIndexDataPath: string = '/Pub/GetIndexData';
/*****获取首页数据*****/
static GetIndexData(lon: number, lat: number, city: string, county: string, sort: number, page: number) {
var result = Service.Request(this.GetIndexDataPath, "GET", { lon, lat, city, county, sort, page });
return result;
}
private static GetMenuDataPath: string = '/Pub/GetMenuData';
/*****获取分类*****/
static GetMenuData(type: string, parent: string) {
var result = Service.Request(this.GetMenuDataPath, "GET", { type, parent });
return result;
}
private static GetMerchDataPath: string = '/Pub/GetMerchData';
/*****获取店铺*****/
static GetMerchData(code: string, serch: string, assId: string, lon: number, lat: number, city: string, county: string, sort: number, page: number, limit: number) {
var result = Service.Request(this.GetMerchDataPath, "GET", { code, serch, assId, lon, lat, city, county, sort, page, limit });
return result;
}
private static GetRandomMerchPath: string = '/Pub/GetRandomMerch';
/*****随机获取商家*****/
static GetRandomMerch(count: number, lon: number, lat: number, city: string, county: string) {
var result = Service.Request(this.GetRandomMerchPath, "GET", { count, lon, lat, city, county });
return result;
}
}
export {
Service,
NvpPubService
}

View File

@@ -1,22 +0,0 @@
import { Service } from '@/Service/Service';
/*****腾讯云存储*****/
class NvpTencentCosService {
private static GetAuthorizationPath: string = '/TencentCos/GetAuthorization';
/*****获取云存储配置*****/
static GetAuthorization() {
var result = Service.Request(this.GetAuthorizationPath, "GET", "");
return result;
}
private static GetUpLoadInfoPath: string = '/TencentCos/GetUpLoadInfo';
/*****获取上传地址*****/
static GetUpLoadInfo(code: string, fileName: string, desire: string) {
var result = Service.Request(this.GetUpLoadInfoPath, "GET", { code, fileName, desire });
return result;
}
}
export {
Service,
NvpTencentCosService
}

View File

@@ -1,43 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpUserService {
private static GetUserInfoPath: string = '/User/GetUserInfo';
/*****用户基础信息*****/
static GetUserInfo() {
var result = Service.Request(this.GetUserInfoPath, "GET", "");
return result;
}
private static UpdateUserInfoPath: string = '/User/UpdateUserInfo';
/*****修改用户信息*****/
static UpdateUserInfo(par: string, smsCode: string, type: number) {
var result = Service.Request(this.UpdateUserInfoPath, "POST", { par, smsCode, type });
return result;
}
private static GetUserAccLogPath: string = '/User/GetUserAccLog';
/*****获取账户记录*****/
static GetUserAccLog(code: string, accType: string, page: number) {
var result = Service.Request(this.GetUserAccLogPath, "GET", { code, accType, page });
return result;
}
private static GetUserCommissionInfoPath: string = '/User/GetUserCommissionInfo';
/*****用户佣金账户*****/
static GetUserCommissionInfo() {
var result = Service.Request(this.GetUserCommissionInfoPath, "GET", "");
return result;
}
private static GetUserCommissionLogPath: string = '/User/GetUserCommissionLog';
/*****用户佣金记录*****/
static GetUserCommissionLog(code: string, accType: string, page: number) {
var result = Service.Request(this.GetUserCommissionLogPath, "GET", { code, accType, page });
return result;
}
}
export {
Service,
NvpUserService
}

View File

@@ -1,22 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpWithService {
private static WithDrawPath: string = '/With/WithDraw';
/*****佣金提现*****/
static WithDraw(money: number, name: string, account: string) {
var result = Service.Request(this.WithDrawPath, "POST", { money, name, account });
return result;
}
private static GetUserWithLogPath: string = '/With/GetUserWithLog';
/*****用户提现记录*****/
static GetUserWithLog(page: number) {
var result = Service.Request(this.GetUserWithLogPath, "GET", { page });
return result;
}
}
export {
Service,
NvpWithService
}

View File

@@ -15,6 +15,16 @@ export class Service extends BaseConfig {
}
}
//获取静态图片地址
static GetIconImg(path : string) {
return path;
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.payuploadUrl}${path}`;
}
}
//获取图片地址
static GetMateUrlByImg(path : string) {
if (path.startsWith('http') || path.startsWith('https')) {
@@ -175,6 +185,21 @@ export class Service extends BaseConfig {
})
}
static Confirm(msg : string, cb ?: any) {
uni.showModal({
title: '确认',
content: msg,
showCancel: true,
cancelText: '取消',
confirmText: '确定',
success: res => {
if (res.confirm) {
cb && cb();
}
}
})
}
static LoadIng(text : any) : void {
uni.showLoading({
title: text,
@@ -240,6 +265,13 @@ export class Service extends BaseConfig {
// 时间戳处理
static formatDate(time : any, type : number) : string {
if(time==0 && type==6 ){
return "00:00"
}
if(time==0 && type==5 ){
return "00"
}
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始所以加1并用0填充
@@ -258,11 +290,14 @@ export class Service extends BaseConfig {
return `${hours}:${minutes}`;
} else if (type == 4) {
return `${year}${month}${day}`;
}else if (type == 5) {
return `${seconds}`;
}
else {
return `${hours}:${minutes}`;
}
}
/*****节流*****/
@@ -301,7 +336,7 @@ export class Service extends BaseConfig {
static uploadH5(path, dic, callback) {
console.log(this.payuploadUrl,'xxx')
uni.uploadFile({
url: this.payuploadUrl+'/Upload/UploadFile',
url: this.payuploadUrl+'/api/Upload/UploadImg',
method: "POST",
header: {
'Authorization': 'Bearer ' + Service.GetUserToken(),

View File

@@ -0,0 +1,126 @@
import { Service } from '@/Service/Service';
/*****项目接口*****/
class PlanService {
private static GetPlanListPath : string = '/api/Plan/GetPlanList';
/*****计划列表接口*****/
static GetPlanList( planType:string, page:string) {
var result = Service.Request(this.GetPlanListPath, "GET", { planType, page});
return result;
}
private static GetPlanInfoPath : string = '/api/Plan/GetPlanInfo';
/*****计划详情接口*****/
static GetPlanInfo(planId:string) {
var result = Service.Request(this.GetPlanInfoPath, "GET", {planId});
return result;
}
private static AddPlanPath : string = '/api/Plan/AddPlan';
/*****添加修改计划接口*****/
static AddPlan(data:any) {
var result = Service.Request(this.AddPlanPath, "POST", data);
return result;
}
private static DeletePlanPath : string = '/api/Plan/DeletePlan';
/*****删除计划接口*****/
static DeletePlan(planId:string) {
var result = Service.Request(this.DeletePlanPath, "POST", { planId});
return result;
}
private static GetNoSelectStudentPath : string = '/api/Plan/GetNoSelectStudent';
/*****查询项目分组未选择的学生接口*****/
static GetNoSelectStudent(planId:string) {
var result = Service.Request(this.GetNoSelectStudentPath, "GET", { planId});
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 {
Service,
PlanService
}

View File

@@ -0,0 +1,14 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class loginService {
private static LoginPath : string = '/api/Login/Login';
/*****登录接口*****/
static Login(code : string) {
var result = Service.Request(this.LoginPath, "POST", { code });
return result;
}
}
export {
Service,
loginService
}

View File

@@ -0,0 +1,47 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class studentService {
private static GetStudentListPath : string = '/api/Student/GetStudentList';
/*****学生列表不分页接口*****/
static GetStudentList(planId:string) {
var result = Service.Request(this.GetStudentListPath, "GET", { planId });
return result;
}
private static GetStudentInfoPath : string = '/api/Student/GetStudentInfo';
/*****学生详情接口*****/
static GetStudentInfo(studentId : string) {
var result = Service.Request(this.GetStudentInfoPath, "GET", { studentId });
return result;
}
private static AddPath : string = '/api/Student/Add';
/*****添加学生接口*****/
static Add(data:any) {
var result = Service.Request(this.AddPath, "POST", data);
return result;
}
private static DeletePath : string = '/api/Student/Delete';
/*****删除学生接口*****/
static Delete(studentId:string) {
var result = Service.Request(this.DeletePath, "POST", {studentId });
return result;
}
private static GetStudentListPagePath : string = '/api/Student/GetStudentListPage';
/*****学生列表分页接口*****/
static GetStudentListPage(page : string) {
var result = Service.Request(this.GetStudentListPagePath, "GET", { page });
return result;
}
}
export {
Service,
studentService
}

View File

@@ -0,0 +1,14 @@
import { Service } from '@/Service/Service';
/*****上传接口*****/
class loginService {
private static UploadImgPath : string = '/api/Upload/UploadImg';
/*****上传接口*****/
static UploadImg(file : string,path:string) {
var result = Service.Request(this.UploadImgPath, "POST", { file, path});
return result;
}
}
export {
Service,
loginService
}

View File

@@ -0,0 +1,69 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class userService {
private static GetUserInfoPath : string = '/api/User/GetUserInfo';
/*****获取用户信息接口*****/
static GetUserInfo() {
var result = Service.Request(this.GetUserInfoPath, "GET", {});
return result;
}
private static IsCheakUserVerifyPath : string = '/api/User/IsCheakUserVerify';
/*****是否可以使用接口*****/
static IsCheakUserVerify() {
var result = Service.Request(this.IsCheakUserVerifyPath, "GET", {});
return result;
}
private static UpdateUserInfoPath : string = '/api/User/UpdateUserInfo';
/*****修改用户信息接口*****/
static UpdateUserInfo(name:string,phone:string,headImg:string) {
var result = Service.Request(this.UpdateUserInfoPath, "POST", {name,phone,headImg});
return result;
}
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) {
// var result = Service.Request(this.GetStudentInfoPath, "GET", {studentId});
// return result;
// }
}
export {
Service,
userService
}

View File

@@ -0,0 +1,19 @@
export interface Project {
id: string;
name: string;
createdAt: number;
}
export interface StudentRecord {
id: string;
projectId: string;
name: string;
time: number;
createdAt: number;
}
export interface TimingState {
isRunning: boolean;
startTime: number;
elapsedTime: number;
}

View File

@@ -0,0 +1,69 @@
import type { Project, StudentRecord } from '../Domain/Project';
const STORAGE_KEYS = {
PROJECTS: 'swimming_projects',
RECORDS: 'swimming_records',
};
export class StorageManager {
static async getProjects(): Promise<Project[]> {
try {
const data = uni.getStorageSync(STORAGE_KEYS.PROJECTS);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
}
static async saveProjects(projects: Project[]): Promise<void> {
uni.setStorageSync(STORAGE_KEYS.PROJECTS, JSON.stringify(projects));
}
static async addProject(project: Project): Promise<void> {
const projects = await this.getProjects();
projects.unshift(project);
await this.saveProjects(projects);
}
static async deleteProject(projectId: string): Promise<void> {
const projects = await this.getProjects();
const filtered = projects.filter(p => p.id !== projectId);
await this.saveProjects(filtered);
}
static async getRecordsByProject(projectId: string): Promise<StudentRecord[]> {
try {
const data = uni.getStorageSync(STORAGE_KEYS.RECORDS);
const records: StudentRecord[] = data ? JSON.parse(data) : [];
return records.filter(r => r.projectId === projectId);
} catch {
return [];
}
}
static async saveRecords(records: StudentRecord[]): Promise<void> {
uni.setStorageSync(STORAGE_KEYS.RECORDS, JSON.stringify(records));
}
static async addRecord(record: StudentRecord): Promise<void> {
try {
const data = uni.getStorageSync(STORAGE_KEYS.RECORDS);
const records: StudentRecord[] = data ? JSON.parse(data) : [];
records.unshift(record);
await this.saveRecords(records);
} catch {
const records: StudentRecord[] = [record];
await this.saveRecords(records);
}
}
static async deleteRecord(recordId: string): Promise<void> {
try {
const data = uni.getStorageSync(STORAGE_KEYS.RECORDS);
const records: StudentRecord[] = data ? JSON.parse(data) : [];
const filtered = records.filter(r => r.id !== recordId);
await this.saveRecords(filtered);
} catch {
}
}
}

View File

@@ -0,0 +1,22 @@
export function formatTime(ms: number): string {
if (ms < 0) ms = 0;
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const milliseconds = Math.floor((ms % 1000) / 10);
const hh = hours.toString().padStart(2, '0');
const mm = minutes.toString().padStart(2, '0');
const ss = seconds.toString().padStart(2, '0');
const mmm = milliseconds.toString().padStart(2, '0');
if (hours > 0) {
return `${hh}:${mm}:${ss}.${mmm}`;
}
return `${mm}:${ss}.${mmm}`;
}
export function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

View File

@@ -1,5 +1,5 @@
{
"name" : "科讯代购",
"name" : "智能秒表",
"appid" : "__UNI__06C2D6A",
"description" : "",
"versionName" : "1.0.8",
@@ -112,7 +112,7 @@
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"appid" : "wx6a9cfc961962b249",
"setting" : {
"urlCheck" : false
},

View File

@@ -8,46 +8,175 @@
}
},
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "门店运营",
"navigationBarBackgroundColor": "#36394D",
"navigationStyle": "custom",
"backgroundColor": "#F8F8F8"
"navigationBarTitleText": "智能秒表"
}
},
{
"path" : "pages/index/user",
"style" :
{
"navigationBarTitleText" : "我的"
"path": "pages/index/user",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"subPackages": [{
"root": "pages/userFunc",
"pages": [{
"path": "setCourse",
"style": {
"navigationBarTitleText": "新增项目"
}
},
{
"path": "swiming",
"style": {
"navigationBarTitleText": "计时项目"
}
},
{
"path": "segmentation",
"style": {
"navigationBarTitleText": "分段"
}
},
{
"path": "student",
"style": {
"navigationBarTitleText": "学员管理"
}
},
{
"path": "analyze",
"style": {
"navigationBarTitleText": "数据分析"
}
},
{
"path": "dataAnalyze",
"style": {
"navigationBarTitleText": "数据分析图"
}
},
{
"path": "set",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "projectList",
"style": {
"navigationBarTitleText": "项目列表"
}
},
{
"path": "project",
"style": {
"navigationBarTitleText": "包干",
"pageOrientation": "auto",
"navigationStyle": "custom"
}
},
{
"path": "plan",
"style": {
"navigationBarTitleText": "训练计划"
}
},
{
"path": "hunyang",
"style": {
"navigationBarTitleText": "混氧",
"pageOrientation": "auto",
"navigationStyle": "custom"
}
},
{
"path": "addHunyang",
"style": {
"navigationBarTitleText": "创建项目"
}
},
{
"path": "cs",
"style": {
"navigationBarTitleText": "测试",
"pageOrientation": "auto"
}
},
{
"path": "userPermission",
"style": {
"navigationBarTitleText": "权限管理"
}
},
{
"path": "contactManage",
"style": {
"navigationBarTitleText": "联系方式管理"
}
}
]
}, {
"root": "pages/dataAnalyze",
"pages": [{
"path": "baoganAnalyze",
"style": {
"navigationBarTitleText": "包干数据"
}
},
{
"path": "timingAnalze",
"style": {
"navigationBarTitleText": "计时数据"
}
},
{
"path": "Curve",
"style": {
"navigationBarTitleText": "曲线走势图"
}
},
{
"path": "paragraphAnalyze",
"style": {
"navigationBarTitleText": "分段数据"
}
},
{
"path": "grades",
"style": {
"navigationBarTitleText": "成绩排名"
}
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "v派商家",
"navigationBarBackgroundColor": "#F84F28",
"backgroundColor": "#F8F8F8"
"navigationBarBackgroundColor": "#fff",
"backgroundColor": "#000"
},
"tabBar": {
"color": "#000",
"selectedColor": "#000",
"backgroundColor": "#FFFFFF",
"list": [ {
"pagePath": "pages/index/index",
"iconPath": "static/tab/01.png",
"selectedIconPath": "static/tab/02.png",
"text": "主页"
}, {
"pagePath": "pages/index/user",
"iconPath": "static/tab/01.png",
"selectedIconPath": "static/tab/02.png",
"text": "我的"
}
"list": [{
"pagePath": "pages/index/index",
"iconPath": "static/tab/01.png",
"selectedIconPath": "static/tab/02.png",
"text": "项目"
},
{
"pagePath": "pages/index/user",
"iconPath": "static/tab/03.png",
"selectedIconPath": "static/tab/04.png",
"text": "我的"
}
]
}
}

View File

@@ -0,0 +1,719 @@
<template>
<view class="curve-container">
<!-- 项目选择卡片 -->
<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>
<!-- 项目选择弹窗 -->
<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 class="filter-card " v-if="selectProcet">
<view class="card-title">
<u-icon name="calendar" size="18" color="#13c2c2"></u-icon>
<text>日期范围</text>
</view>
<view class="date-filter" @click="openCalendar">
<view class="date-item">
<text class="date-label">开始日期</text>
<text class="date-value" :class="{ placeholder: !begin }">{{ begin || '请选择' }}</text>
</view>
<view class="date-divider"></view>
<view class="date-item">
<text class="date-label">结束日期</text>
<text class="date-value" :class="{ placeholder: !end }">{{ end || '请选择' }}</text>
</view>
<u-icon name="arrow-right" size="16" color="#bbb" class="date-arrow"></u-icon>
</view>
</view>
<!-- 学生选择卡片 -->
<view class="filter-card " v-if="selectProcet && begin && projectStudents.length > 0">
<view class="section-header">
<view class="card-title">
<u-icon name="account" size="18" color="#52c41a"></u-icon>
<text>选择学员</text>
</view>
<text class="select-count">已选 {{ selectedStudentIds.length }}/3</text>
</view>
<view class="student-list">
<view v-for="student in projectStudents" :key="student.studentId"
:class="['student-item', { selected: selectedStudentIds.includes(student.studentId) }]"
@click="toggleStudent(student.studentId)">
<text class="student-name">{{ student.name }}</text>
<view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)">
<u-icon name="checkmark" size="14" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<!-- 图表区域 -->
<view class="chart-section" v-if="selectedStudentIds.length > 0 && !showCalendar">
<!-- 统计概览 -->
<view class="stats-card">
<view class="stat-item">
<text class="stat-label">选中项目</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">{{ selectedStudentIds.length }} </text>
</view>
</view>
<!-- 折线图 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">成绩趋势对比</text>
<text class="chart-desc">历史训练成绩变化曲线</text>
</view>
<scroll-view class="chart-scroll" scroll-x :show-scrollbar="false">
<view class="chart-box" :style="{ width: chartWidth + 'rpx' }">
<qiun-data-charts type="line" :opts="lineOpts" :chartData="lineChartData" :ontouch="true" />
</view>
</scroll-view>
</view>
</view>
<!-- 日历弹窗 -->
<uni-calendar ref="calendar" :insert="false" range='true' :lunar="true" @confirm="calendarConfirm" />
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
import { studentService } from '@/Service/swimming/studentService'
const MAX_SELECTED_STUDENTS = 3
const chartColors = ['#52c41a', '#1890ff', '#faad14']
let showProject = ref(false)
let selectProcet = ref('')
let selectId = ref('')
const begin = ref<string>('')
const end = ref<string>('')
const showCalendar = ref(false)
const projectOptions = ref<Array<any>>([[]])
interface Student {
studentId : string
name : string
}
interface StudentTrainingRecord {
studentId : string
studentName : string
time : number
recordDate : string
recordFullDate : string
round : number
}
const projectStudents = ref<Student[]>([])
const selectedStudentIds = ref<string[]>([])
const selectedStudents = computed(() => {
return projectStudents.value.filter(student =>
selectedStudentIds.value.includes(student.studentId)
)
})
const chartWidth = computed(() => {
const categoryCount = lineChartData.value.categories.length
if (categoryCount <= 6) return 700
return Math.max(700, categoryCount * 100)
})
const lineOpts = ref({
color: chartColors,
padding: [15, 10, 0, 15],
legend: {
show: true,
position: 'bottom',
itemGap: 30
},
xAxis: {
disableGrid: true,
rotateLabel: true,
scrollShow: true,
scrollAlign: 'left'
},
yAxis: {
data: [{
min: 0,
format: (val : number) => val.toFixed(1) + 's'
}]
},
extra: {
line: {
type: 'curve',
width: 2,
activeType: 'hollow',
linearType: 'none',
onGap: false
}
}
})
const lineChartData = ref({
categories: [] as string[],
series: [] as any[]
})
let calendar=ref(null)
onLoad(() => {
getProjectData()
})
onShow(() => { })
const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => {
if (res.code == 0) {
projectOptions.value[0] = res.data.list
} else {
Service.Msg(res.msg)
}
})
}
const selectProcetFunc = (project : any) => {
selectProcet.value = project.name
selectId.value = project.planId
showProject.value = false
selectedStudentIds.value = []
begin.value = ''
end.value = ''
projectStudents.value = []
lineChartData.value.categories = []
lineChartData.value.series = []
loadProjectStudents(selectId.value)
}
const openCalendar = () => {
showCalendar.value = true
calendar.value.open()
}
const calendarConfirm = (e : any) => {
const newDate = e.range
if (e.range.before !== begin.value || e.range.after !== end.value ) {
selectedStudentIds.value = []
lineChartData.value.categories = []
lineChartData.value.series = []
}
begin.value = newDate.before
if( e.range.after){
end.value = e.range.after
}else{
end.value = e.range.before
}
showCalendar.value = false
}
const calendarClose = () => {
showCalendar.value = false
}
const loadProjectStudents = (projectId : string) => {
studentService.GetStudentList('').then(res => {
if (res.code == 0) {
projectStudents.value = res.data
} else {
Service.Msg(res.msg)
}
})
}
const toggleStudent = (studentId : string) => {
const index = selectedStudentIds.value.indexOf(studentId)
if (index !== -1) {
selectedStudentIds.value.splice(index, 1)
} else {
if (selectedStudentIds.value.length >= MAX_SELECTED_STUDENTS) {
Service.Msg(`最多只能选择 ${MAX_SELECTED_STUDENTS} 名学员`)
return
}
selectedStudentIds.value.push(studentId)
}
const studentIdList = selectedStudentIds.value.map((item) => item)
const reqData = {
planId: selectId.value,
studentId: JSON.stringify(studentIdList),
sTime: begin.value,
eTime: end.value
}
PlanService.GetQuxianLog(reqData).then(res => {
if (res.code == 0) {
const list = res.data.list || []
const days = list.map((item : any) => item.dayTime)
// 按已选学生顺序初始化 series缺省日期填充 0
const series = studentIdList.map(id => {
const student = projectStudents.value.find(s => s.studentId === id)
return {
name: student?.name || '',
data: new Array(days.length).fill(0)
}
})
// 建立 studentId -> series 索引映射
const idToIndex : Record<string, number> = {}
studentIdList.forEach((id, idx) => {
idToIndex[id] = idx
})
// 按日期填充数据
list.forEach((dayItem : any, dayIndex : number) => {
; (dayItem.data || []).forEach((record : any) => {
const sIdx = idToIndex[record.studentId]
if (sIdx !== undefined) {
series[sIdx].data[dayIndex] = record.time ?? 0
if (!series[sIdx].name && record.studentName) {
series[sIdx].name = record.studentName
}
}
})
})
lineChartData.value.categories = days
lineChartData.value.series = series
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #e8ecf3;
}
.curve-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
/* 通用卡片 */
.filter-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
// border-left: 8rpx solid #999;
}
.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;
}
}
}
/* 日期选择 */
.date-filter {
display: flex;
align-items: center;
background: #f0fafa;
border-radius: 12rpx;
padding: 22rpx 24rpx;
border: 1rpx solid #d6f0f0;
transition: all 0.2s;
&:active {
background: #e6f5f5;
}
}
.date-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
}
.date-label {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.date-value {
font-size: 30rpx;
color: #333;
font-weight: 600;
&.placeholder {
color: #999;
font-weight: 400;
}
}
.date-divider {
width: 2rpx;
height: 50rpx;
background: #b2dfdb;
margin: 0 20rpx;
}
.date-arrow {
margin-left: 12rpx;
}
/* 学生选择区域 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.select-count {
font-size: 24rpx;
color: #52c41a;
font-weight: 600;
background: #f0f9eb;
padding: 6rpx 16rpx;
border-radius: 20rpx;
}
}
.student-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.student-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 10rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 2rpx solid transparent;
transition: all 0.2s;
position: relative;
&:active {
transform: scale(0.96);
}
&.selected {
background: #f0f9eb;
border-color: #52c41a;
}
.student-avatar {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
.avatar-text {
font-size: 24rpx;
font-weight: 700;
color: #fff;
}
}
.student-name {
font-size: 24rpx;
color: #333;
text-align: center;
font-weight: 500;
}
.check-icon {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 28rpx;
height: 28rpx;
background: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
/* 图表区域 */
.chart-section {
margin-top: 4rpx;
position: relative;
z-index: 1;
}
.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);
// border-left: 8rpx solid #faad14;
.stat-item {
text-align: center;
.stat-label {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
font-weight: 500;
}
.stat-value {
font-size: 32rpx;
font-weight: 700;
color: #faad14;
}
}
.stat-divider {
width: 1rpx;
height: 50rpx;
background-color: #f0f0f0;
}
}
.chart-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
// border-left: 8rpx solid #52c41a;
.chart-header {
margin-bottom: 20rpx;
padding-left: 12rpx;
border-left: 6rpx solid #52c41a;
.chart-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 6rpx;
}
.chart-desc {
font-size: 24rpx;
color: #999;
}
}
.chart-scroll {
width: 100%;
height: 500rpx;
background-color: #fafafa;
border-radius: 12rpx;
overflow: hidden;
white-space: nowrap;
border: 1rpx solid #f0f0f0;
.chart-box {
height: 100%;
display: inline-block;
}
}
}
/* 项目选择弹窗 */
.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-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
}
}
.empty-project {
display: flex;
align-items: center;
justify-content: center;
padding: 60rpx 0;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,516 @@
<template>
<view class="grades-container">
<!-- 项目选择卡片 -->
<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="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">{{ 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>
<!-- 排名列表 -->
<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.studentName }}</text>
<text class="student-speed">最快用时{{ formatTime(item.quicklyTime ) }}</text>
</view>
<view class="speed-detail">
<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>
<script setup lang="ts">
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 projectOptions = ref<Array<any>>([[]])
interface StudentGrade {
id: string
name: string
bestSpeed: number
bestTime: number
recordDate: string
studentId: string
}
const gradeList = ref<Array<any>>([])
const maxSpeed = computed(() => {
if (gradeList.value.length === 0) return 0
return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2)
})
onLoad(() => {
getProjectData()
})
const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => {
if (res.code == 0) {
projectOptions.value[0] = res.data.list
} else {
Service.Msg(res.msg)
}
})
}
const selectProcetFunc = (e: any) => {
selectProcet.value = e.name
selectId.value = e.planId
loadProjectGrades(selectId.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')}`
}
const handleRankingClick = (item: StudentGrade) => {
console.log('点击了排名项:', item)
}
</script>
<style lang="scss" scoped>
page {
background-color: #e8ecf3;
}
.grades-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
/* 通用卡片 */
.filter-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.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;
}
.stat-value {
font-size: 32rpx;
font-weight: 700;
color: #faad14;
}
}
.stat-divider {
width: 1rpx;
height: 50rpx;
background-color: #f0f0f0;
}
}
.ranking-list {
.ranking-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s;
position: relative;
overflow: hidden;
&:active {
transform: scale(0.99);
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
}
&.top-three {
border-left-color: #ffd700;
}
.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-number {
font-size: 24rpx;
font-weight: 700;
color: #999;
}
&.rank-1 {
background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
box-shadow: 0 2rpx 8rpx 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;
}
}
}
.student-info {
flex: 1;
min-width: 0;
.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;
}
}
}
}
/* 空状态 */
.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

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,658 @@
<template>
<view>
</view>
<view class="user-container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-avatar">
<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>
</view>
<view class="user-info">
<text class="user-nickname">{{ userInfo.name }}</text>
<view class="user-phone-wrapper">
<text class="user-phone">{{ userInfo.phone || '暂无手机号' }}</text>
</view>
</view>
<view @click="Service.GoPage('/pages/userFunc/set')" class="user-edit">
<u-icon name="edit-pen" size="20" color="rgba(255,255,255,0.8)"></u-icon>
</view>
</view>
<!-- 统计数据区域 -->
<view class="stats-section">
<view class="stats-card">
<view class="stat-item">
<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 @click="goPage('/pages/userFunc/student')" class="stat-info">
<text class="stat-value">{{ studentCount }}</text>
<text class="stat-label">学员数</text>
</view>
</view>
</view>
</view>
<!-- 功能菜单区域 -->
<view class="menu-section">
<view class="menu-card">
<!-- 项目管理菜单项 -->
<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>
<view class="menu-content">
<view class="menu-header">
<text class="menu-title">项目管理</text>
<view class="menu-tag project-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/student')">
<view class="menu-icon-bg academy-icon-bg">
<u-icon name="grid" size="28" color="#fff"></u-icon>
</view>
<view class="menu-content">
<view class="menu-header">
<text class="menu-title">学员管理</text>
<view class="menu-tag academy-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/analyze')">
<view class="menu-icon-bg analysis-icon-bg">
<u-icon name="order" size="28" color="#fff"></u-icon>
</view>
<view class="menu-content">
<view class="menu-header">
<text class="menu-title">数据分析</text>
<view class="menu-tag analysis-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 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>
<script setup lang="ts">
import { onShow,onLoad } from "@dcloudio/uni-app";
onLoad(() => {
import { onShow, onLoad } from "@dcloudio/uni-app"
import { reactive, ref } from "vue"
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(() => {
onShow(() => {
})
});
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 : any) => {
if (content.code == 0) {
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 goPage = (path : any) => {
if (!isUse.value) {
Service.Msg('用户暂无权限,请等待审核!')
return
}
Service.GoPage(path)
}
const callContact = () => {
uni.makePhoneCall({
phoneNumber: kefuPhone.value,
fail: () => {
Service.Msg('拨打电话失败')
}
})
}
</script>
<style lang="scss">
</style>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.user-container {
padding: 20rpx;
padding-top: 30rpx;
}
/* 用户信息卡片 */
.user-card {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 28rpx;
padding: 40rpx 30rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.25);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: -40%;
right: -30%;
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
pointer-events: none;
}
&::after {
content: '';
position: absolute;
bottom: -30%;
left: -20%;
width: 250rpx;
height: 250rpx;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
pointer-events: none;
}
}
.user-avatar {
position: relative;
z-index: 1;
.avatar-circle {
width: 130rpx;
height: 130rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 5rpx solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(20rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.avatar-image {
width: 130rpx;
height: 130rpx;
border-radius: 50%;
border: 5rpx solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(20rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.avatar-badge {
position: absolute;
bottom: 0;
right: 0;
width: 44rpx;
height: 44rpx;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.4);
}
}
.user-info {
flex: 1;
position: relative;
z-index: 1;
.user-nickname {
font-size: 38rpx;
font-weight: 700;
color: #fff;
margin-bottom: 14rpx;
display: block;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.user-phone-wrapper {
display: flex;
align-items: center;
gap: 8rpx;
.user-phone {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
}
}
}
.user-edit {
width: 60rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10rpx);
position: relative;
z-index: 1;
transition: all 0.3s ease;
&:active {
background: rgba(255, 255, 255, 0.35);
transform: scale(0.95);
}
}
/* 统计数据区域 */
.stats-section {
margin-bottom: 24rpx;
.stats-card {
background-color: #fff;
border-radius: 28rpx;
padding: 36rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.stat-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.stat-icon-bg {
width: 68rpx;
height: 68rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.12);
&.project-stat {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
}
&.record-stat {
background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
}
&.student-stat {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
}
}
.stat-info {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4rpx;
}
.stat-value {
font-size: 42rpx;
font-weight: 700;
background: linear-gradient(135deg, #333 0%, #666 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
}
.stat-label {
font-size: 24rpx;
color: #999;
font-weight: 500;
}
.stat-divider {
width: 1rpx;
height: 80rpx;
background: linear-gradient(180deg, transparent 0%, #e8e8e8 50%, transparent 100%);
}
}
/* 功能菜单区域 */
.menu-section,
.settings-section {
margin-bottom: 24rpx;
.menu-card {
background-color: #fff;
border-radius: 28rpx;
padding: 8rpx 0;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
padding: 36rpx 32rpx;
transition: all 0.3s ease;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
height: 100%;
background: linear-gradient(90deg, rgba(24, 144, 255, 0.06) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
&:active {
&::before {
opacity: 1;
}
}
}
.menu-icon-bg {
width: 84rpx;
height: 84rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
position: relative;
&::after {
content: '';
position: absolute;
top: 6rpx;
right: 6rpx;
width: 18rpx;
height: 18rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
/* 项目管理图标背景色 - 蓝色渐变 */
&.project-icon-bg {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
}
/* 训练计划图标背景色 - 紫色渐变 */
&.plan-icon-bg {
background: linear-gradient(135deg, #722ed1 0%, #9254de 50%, #531dab 100%);
}
/* 学员管理图标背景色 - 绿色渐变 */
&.academy-icon-bg {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #389e0d 100%);
}
/* 数据分析图标背景色 - 橙色渐变 */
&.analysis-icon-bg {
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%);
}
&.about-icon-bg {
background: linear-gradient(135deg, #722ed1 0%, #9254de 50%, #531dab 100%);
}
}
.menu-content {
flex: 1;
}
.menu-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.menu-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.menu-tag {
padding: 4rpx 12rpx;
border-radius: 16rpx;
font-size: 20rpx;
font-weight: 500;
/* 项目管理标签样式 - 蓝色 */
&.project-tag {
background: linear-gradient(135deg, rgba(24, 144, 255, 0.12) 0%, rgba(24, 144, 255, 0.08) 100%);
color: #1890ff;
}
/* 训练计划标签样式 - 紫色 */
&.plan-tag {
background: linear-gradient(135deg, rgba(114, 46, 209, 0.12) 0%, rgba(114, 46, 209, 0.08) 100%);
color: #722ed1;
}
/* 学员管理标签样式 - 绿色 */
&.academy-tag {
background: linear-gradient(135deg, rgba(82, 196, 26, 0.12) 0%, rgba(82, 196, 26, 0.08) 100%);
color: #52c41a;
}
/* 数据分析标签样式 - 橙色 */
&.analysis-tag {
background: linear-gradient(135deg, rgba(250, 173, 20, 0.12) 0%, rgba(250, 173, 20, 0.08) 100%);
color: #faad14;
}
/* 管理员标签样式 - 红色 */
&.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;
}
}
.menu-desc {
font-size: 24rpx;
color: #999;
line-height: 1.4;
}
.menu-arrow {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.04);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.menu-item:active {
.menu-icon-bg {
transform: scale(0.92) translateY(2rpx);
}
.menu-arrow {
background: rgba(24, 144, 255, 0.1);
transform: scale(1.1);
}
}
.menu-divider {
height: 1rpx;
background: linear-gradient(90deg, transparent 0%, #f0f0f0 15%, #f0f0f0 85%, transparent 100%);
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

@@ -0,0 +1,802 @@
<template>
<view class="add-hunyang-container">
<!-- 表单区域 -->
<view class="form-section">
<!-- 项目名称 -->
<view class="form-card">
<view class="form-title">项目信息</view>
<view class="form-group">
<text class="form-label">项目名称</text>
<input class="form-input" v-model="projectName" placeholder="请输入项目名称"
placeholder-class="input-placeholder" />
</view>
</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, planIndex) in group.plans" :key="planIndex" class="plan-item">
<view class="plan-item-header">
<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">
<view class="time-input-wrapper">
<input class="time-input" v-model="item.targetSeconds" type="number"
placeholder="" />
<text class="time-unit"></text>
</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">
<text class="input-label">休息时长</text>
<view class="rest-selector">
<input class="rest-input" v-model="item.restTime" type="number" placeholder="请输入休息时长" />
<text class="time-unit"></text>
</view>
</view>
<!-- 圈数 -->
<view class="input-group">
<text class="input-label">圈数</text>
<view class="lap-selector">
<input class="lap-input" v-model="item.lapCount" type="number" placeholder="请输入圈数" />
<text class="time-unit"></text>
</view>
</view>
</view>
</view>
<!-- 添加计划按钮 -->
<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 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">
<button class="cancel-btn" @click="handleCancel">取消</button>
<button class="confirm-btn" @click="handleSave">保存</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Service } from '@/Service/Service'
import { PlanService } from '@/Service/swimming/PlanService'
import { onLoad } from '@dcloudio/uni-app'
// 计划项接口
interface PlanItem {
name: string
targetSeconds: string
restTime: string
lapCount: string
allTime?: number
}
// 分组接口
interface Group {
id: string
name: string
plans: PlanItem[]
allTime?: number
}
// 项目名称
const projectName = ref('')
// 分组列表
const groups = ref<Group[]>([])
let planId = ref('')
onLoad((data: any) => {
planId.value = data.id
if (planId.value) {
getPlanInfo()
} else {
// 新建时默认添加一个分组
addGroup()
}
})
// 获取计划详情
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
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)
}
})
}
// 计算计划总时长(秒)
const totalDuration = computed(() => {
let total = 0
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 => {
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 => {
return Date.now().toString() + Math.random().toString(36).substr(2, 9)
}
// 添加分组
const addGroup = () => {
const newGroup: Group = {
id: generateId(),
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: ''
}
groups.value[groupIndex].plans.push(newItem)
}
// 删除计划项
const deletePlanItem = (groupIndex: number, planIndex: number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除该计划吗?',
success: (res) => {
if (res.confirm) {
groups.value[groupIndex].plans.splice(planIndex, 1)
Service.Msg('删除成功')
}
}
})
}
// 验证表单
const validateForm = (): boolean => {
if (!projectName.value.trim()) {
Service.Msg('请输入项目名称')
return false
}
if (groups.value.length === 0) {
Service.Msg('请至少添加一个分组')
return false
}
for (let g = 0; g < groups.value.length; g++) {
const group = groups.value[g]
if (!group.name.trim()) {
Service.Msg(`分组 ${g + 1} 请输入分组名称`)
return false
}
if (group.plans.length === 0) {
Service.Msg(`分组 "${group.name}" 请至少添加一个计划`)
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
}
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: String(groups.value.length),
subsectionDistance: '',
subsectionInt: '',
users: '',
project: JSON.stringify(projectData),
group: ''
}
PlanService.AddPlan(data).then(res => {
if (res.code == 0) {
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)
}
})
}
// 取消
const handleCancel = () => {
uni.showModal({
title: '确认取消',
content: '确定要取消吗?已输入的内容将不会保存。',
success: (res) => {
if (res.confirm) {
Service.GoPageBack()
}
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.add-hunyang-container {
min-height: 100vh;
padding-bottom: 140rpx;
}
/* 表单区域 */
.form-section {
padding: 20rpx;
}
.form-card {
background-color: #fff;
border-radius: 24rpx;
padding: 32rpx 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.form-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-bottom: 28rpx;
display: block;
}
}
/* 分组头部 */
.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;
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 14rpx;
}
.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);
}
}
}
/* 计划总时长 */
.total-time-section {
margin-top: 28rpx;
padding-top: 24rpx;
border-top: 2rpx solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.total-time-label {
font-size: 28rpx;
font-weight: 600;
color: #666;
}
.total-time-value {
font-size: 36rpx;
font-weight: bold;
color: #1890ff;
font-family: 'DIN Alternate', 'Helvetica Neue', monospace;
letter-spacing: 2rpx;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
padding: 12rpx 24rpx;
border-radius: 12rpx;
border: 2rpx solid #91d5ff;
}
}
/* 计划项 */
.plan-item {
background-color: #f9f9f9;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
border: 2rpx solid #f0f0f0;
.plan-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.plan-item-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.delete-plan-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);
}
}
}
.plan-item-content {
display: flex;
flex-direction: column;
gap: 20rpx;
}
}
/* 输入组 */
.input-group {
display: flex;
align-items: center;
justify-content: space-between;
.input-label {
font-size: 26rpx;
font-weight: 500;
color: #666;
min-width: 120rpx;
}
}
/* 时间选择器 */
.time-selector {
display: flex;
align-items: center;
gap: 8rpx;
flex: 1;
.time-input-wrapper {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 16rpx;
height: 72rpx;
flex: 1;
.time-input {
flex: 1;
font-size: 28rpx;
color: #333;
text-align: right;
}
.time-unit {
font-size: 24rpx;
color: #999;
margin-left: 8rpx;
}
}
.time-separator {
font-size: 32rpx;
font-weight: bold;
color: #333;
padding: 0 4rpx;
}
}
/* 休息选择器 */
.rest-selector,
.lap-selector {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 16rpx;
height: 72rpx;
flex: 1;
.rest-input,
.lap-input {
flex: 1;
font-size: 28rpx;
color: #333;
text-align: right;
}
.time-unit {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
/* 计划名称输入 */
.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;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 24rpx 32rpx;
margin-top: 20rpx;
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
border-radius: 16rpx;
border: 2rpx dashed #91d5ff;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
background: linear-gradient(135deg, #e6f7ff 0%, #d6f4ff 100%);
}
.add-plan-text {
font-size: 28rpx;
color: #1890ff;
font-weight: 500;
}
}
/* 添加分组按钮 */
.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;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 99;
.action-buttons {
display: flex;
gap: 16rpx;
}
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
border: none;
font-size: 30rpx;
font-weight: 600;
transition: all 0.25s ease;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
&:active {
background-color: #e8e8e8;
transform: scale(0.96);
}
}
.confirm-btn {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
&:active {
transform: scale(0.96);
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.25);
}
}
</style>

View File

@@ -0,0 +1,287 @@
<template>
<view class="analyze-container">
<!-- 页面标题区域 -->
<view class="header-section">
<view class="header-title">
<text class="title">数据分析</text>
<text class="subtitle">训练数据统计与分析</text>
</view>
</view>
<!-- 功能入口卡片列表 -->
<view class="function-list">
<!-- 包干数据 -->
<view class="function-card package-card" @click="Service.GoPage('/pages/dataAnalyze/baoganAnalyze')">
<view class="card-icon package-icon">
<u-icon name="grid" size="36" color="#fff"></u-icon>
</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="#ccc"></u-icon>
</view>
</view>
<!-- 计时数据 -->
<view class="function-card timing-card" @click="Service.GoPage('/pages/dataAnalyze/timingAnalze')">
<view class="card-icon timing-icon">
<u-icon name="clock" size="36" color="#fff"></u-icon>
</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="#ccc"></u-icon>
</view>
</view>
<!-- 曲线走势图 -->
<view class="function-card chart-card" @click="Service.GoPage('/pages/dataAnalyze/Curve')">
<view class="card-icon chart-icon">
<u-icon name="calendar" size="36" color="#fff"></u-icon>
</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="#ccc"></u-icon>
</view>
</view>
<!-- 分段数据 -->
<view class="function-card segment-card" @click="Service.GoPage('/pages/dataAnalyze/paragraphAnalyze')">
<view class="card-icon segment-icon">
<u-icon name="list" size="36" color="#fff"></u-icon>
</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="#ccc"></u-icon>
</view>
</view>
<!-- 成绩排名 -->
<view class="function-card ranking-card" @click="Service.GoPage('/pages/dataAnalyze/grades')">
<view class="card-icon ranking-icon">
<u-icon name="integral" size="36" color="#fff"></u-icon>
</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="#ccc"></u-icon>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { ref } from 'vue'
// 功能入口数据统计
const packageCount = ref(18)
const timingCount = ref(36)
const chartCount = ref(12)
const segmentCount = ref(24)
const rankingCount = ref(15)
onLoad(() => {
})
onShow(() => {
})
// 跳转到包干数据
const goToPackageData = () => {
Service.GoPage('/pages/userFunc/project')
}
// 跳转到计时数据
const goToTimingData = () => {
Service.Msg('计时数据功能开发中')
}
// 跳转到曲线走势图
const goToChart = () => {
Service.GoPage('/pages/userFunc/dataAnalyze')
}
// 跳转到分段数据
const goToSegment = () => {
Service.GoPage('/pages/userFunc/segmentation')
}
// 跳转到成绩排名
const goToRanking = () => {
Service.Msg('成绩排名功能开发中')
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.analyze-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
/* 页面标题区域 */
.header-section {
margin-bottom: 24rpx;
.header-title {
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #999;
}
}
}
/* 功能入口卡片列表 */
.function-list {
.function-card {
background-color: #fff;
border-radius: 20rpx;
padding: 32rpx 28rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
opacity: 0;
transition: opacity 0.3s ease;
}
&:active {
transform: scale(0.98);
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.04);
&::before {
opacity: 1;
}
}
&.package-card::before {
background: linear-gradient(180deg, #faad14 0%, #ffc53d 100%);
}
&.timing-card::before {
background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
}
&.chart-card::before {
background: linear-gradient(180deg, #52c41a 0%, #73d13d 100%);
}
&.segment-card::before {
background: linear-gradient(180deg, #722ed1 0%, #9254de 100%);
}
&.ranking-card::before {
background: linear-gradient(180deg, #eb2f96 0%, #f759ab 100%);
}
.card-icon {
width: 80rpx;
height: 80rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 24rpx;
&.package-icon {
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
box-shadow: 0 4rpx 12rpx rgba(250, 173, 20, 0.3);
}
&.timing-icon {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
&.chart-icon {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
}
&.segment-icon {
background: linear-gradient(135deg, #722ed1 0%, #9254de 100%);
box-shadow: 0 4rpx 12rpx rgba(114, 46, 209, 0.3);
}
&.ranking-icon {
background: linear-gradient(135deg, #eb2f96 0%, #f759ab 100%);
box-shadow: 0 4rpx 12rpx rgba(235, 47, 150, 0.3);
}
}
.card-info {
flex: 1;
margin-right: 20rpx;
.card-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.card-desc {
font-size: 24rpx;
color: #999;
display: block;
}
}
.card-arrow {
flex-shrink: 0;
transition: transform 0.3s ease;
}
&:active .card-arrow {
transform: translateX(6rpx);
}
}
}
</style>

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

View File

@@ -0,0 +1,280 @@
<template>
<view class="data-analyze-container">
<!-- 页面标题区域 -->
<view class="header-section">
<view class="header-title">
<text class="title">数据图表</text>
<text class="subtitle">训练数据可视化分析</text>
</view>
</view>
<!-- 多人折线图 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">多人成绩趋势</text>
<text class="chart-desc">近5次训练成绩对比</text>
</view>
<view class="chart-box">
<qiun-data-charts
type="line"
:opts="lineOpts"
:chartData="lineChartData"
:ontouch="true"
/>
</view>
</view>
<!-- 完成率饼状图 - 每个学生 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">学员完成率</text>
<text class="chart-desc">各学员训练完成情况统计</text>
</view>
<view class="chart-box">
<qiun-data-charts
type="pie"
:opts="completionOpts"
:chartData="completionChartData"
:ontouch="true"
/>
</view>
</view>
<!-- 排名统计图 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">学员排名</text>
<text class="chart-desc">按最佳成绩排名</text>
</view>
<view class="chart-box">
<qiun-data-charts
type="column"
:opts="columnOpts"
:chartData="columnChartData"
:ontouch="true"
/>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app"
import { ref, computed, } from 'vue'
// 折线图配置
const lineOpts = ref({
color: ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1'],
padding: [15, 10, 0, 15],
legend: {},
xAxis: {
disableGrid: true,
itemCount: 4
},
yAxis: {
data: [{ min: 0 }]
},
extra: {
line: {
type: 'curve',
width: 2,
activeType: 'hollow'
}
}
})
const lineChartData = ref({
categories: ['第1次', '第2次', '第3次', '第4次', '第5次'],
series: [
{
name: '张三',
data: [45, 42, 38, 40, 36]
},
{
name: '李四',
data: [50, 48, 45, 44, 42]
},
{
name: '王五',
data: [55, 52, 50, 48, 45]
}
]
})
// 完成率饼状图配置
const completionOpts = ref({
color: ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'],
padding: [5, 5, 5, 5],
legend: {},
extra: {
pie: {
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
ringWidth: 40,
title: {
name: '总完成率',
fontSize: 15,
color: '#666666'
},
subtitle: {
name: '87%',
fontSize: 20,
color: '#1890ff'
}
}
}
})
const completionChartData = ref({
series: [
{
name: '完成率',
data: [
{ value: 17, name: '张三' },
{
value: 17,
name: '李四',
color: '#52c41a'
},
{
value: 17,
name: '王五',
color: '#faad14'
},
{
value: 14,
name: '赵六',
color: '#f5222d'
},
{
value: 14,
name: '钱七',
color: '#722ed1'
},
{
value: 11,
name: '孙八',
color: '#13c2c2'
}
]
}
]
})
// 柱状图配置
const columnOpts = ref({
color: ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'],
padding: [15, 10, 0, 15],
legend: {},
xAxis: {
disableGrid: true
},
yAxis: {
data: [{ min: 0, max: 60 }]
},
extra: {
column: {
type: 'group',
width: 30,
activeBgColor: '#000000',
activeBgOpacity: 0.08,
linearType: 'custom',
seriesGap: 2,
categoryGap: 0.5,
barBorderCircle: true
}
}
})
const columnChartData = ref({
categories: ['张三', '李四', '王五', '赵六', '钱七', '孙八'],
series: [
{
name: '最佳成绩(秒)',
data: [36, 42, 45, 50, 56, 60]
}
]
})
onLoad(() => {
})
onShow(() => {
})
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.data-analyze-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
/* 页面标题区域 */
.header-section {
margin-bottom: 24rpx;
.header-title {
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #999;
}
}
}
/* 图表卡片 */
.chart-card {
background-color: #fff;
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:active {
transform: scale(0.995);
}
.chart-header {
margin-bottom: 20rpx;
padding-left: 8rpx;
border-left: 6rpx solid #1890ff;
.chart-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 6rpx;
}
.chart-desc {
font-size: 24rpx;
color: #999;
}
}
.chart-box {
width: 100%;
height: 500rpx;
background-color: #fafafa;
border-radius: 16rpx;
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,991 @@
<template>
<up-navbar v-if="!isLandscape" title="混氧项目" placeholder='true' :autoBack="true">
</up-navbar>
<view class="hunyang-container">
<!-- 项目信息头部 -->
<view class="card-section header-section">
<view class="header-content">
<view class="project-name">{{ name }}</view>
<view class="header-action" :style="{ 'margin-right': isLandscape?'70rpx':'0' } "
@click="Service.GoPage('/pages/userFunc/addHunyang?id='+planId)">
<up-icon name="setting" size="22"></up-icon>
</view>
</view>
</view>
<!-- 分组训练区域 -->
<view class="card-section groups-section" v-if="groupStates.length > 0">
<view class="groups-list">
<view v-for="(groupState, groupIndex) in groupStates" :key="groupIndex" class="group-card" :class="{
resting: groupState.isResting && !groupState.isCompleted,
completed: groupState.isCompleted
}">
<!-- 分组头部 -->
<view class="group-header">
<view class="group-title-area">
<view class="group-name">{{ groupState.groupName }}</view>
</view>
<view class="plan-name">{{ groupState.currentPlan?.name }}</view>
<text class="group-plan-index">
{{ groupState.allTime }}
<!-- {{ groupState.currentPlanIndex }}/{{ groupState.plans.length }} -->
</text>
</view>
<!-- 当前计划内容 -->
<view class="plan-content" v-if="!groupState.isCompleted">
<!-- 倒计时 -->
<view class="countdown-box" :style="{ 'margin': !isLandscape? '23rpx 0':'4rpx 0' }"
:class="{ resting: groupState.isResting }">
<view class="countdown-number"
:style="{transform: !isLandscape? 'scale(2, 2)':'scale(1.3, 1.4 )'}">
{{ formatCountdown(groupState.isResting ? groupState.restCountdown : groupState.countdown) }}
</view>
</view>
<!-- 计划详情 -->
<view class="plan-details">
<view v-if="!isLandscape" class="group-controls">
<view v-if="!groupState.isRunning && !groupState.isCompleted"
class="group-control-btn group-start-btn" @click="startGroup(groupState)">
<u-icon name="play-circle" size="24" color="#fff"></u-icon>
<text>开始</text>
</view>
<view v-else-if="groupState.isRunning" class="group-control-btn group-pause-btn"
@click="pauseGroup(groupState)">
<u-icon name="pause-circle" size="24" color="#fff"></u-icon>
<text>暂停</text>
</view>
</view>
<text class="detail-badge">
M:{{ formatCountdown(groupState.currentPlan?.target) }}
</text>
<text class="detail-badge">
X:{{ groupState.currentPlan?.rest }}
</text>
<text class="detail-badge">
Q: {{ groupState.completedLaps }}/{{ groupState.currentPlan?.circle }}
</text>
<view v-if="!isLandscape" class="group-controls">
<view class="group-control-btn group-reset-btn" @click="resetGroup(groupIndex)">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text>重置</text>
</view>
</view>
</view>
<!-- 分组控制按钮 -->
<view v-if="isLandscape" class="group-controls">
<view v-if="!groupState.isRunning && !groupState.isCompleted"
class="group-control-btn group-start-btn" @click="startGroup(groupState)">
<u-icon name="play-circle" size="24" color="#fff"></u-icon>
<text>开始</text>
</view>
<view v-else-if="groupState.isRunning" class="group-control-btn group-pause-btn"
@click="pauseGroup(groupState)">
<u-icon name="pause-circle" size="24" color="#fff"></u-icon>
<text>暂停</text>
</view>
<view class="group-control-btn group-reset-btn" @click="resetGroup(groupIndex)">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text>重置</text>
</view>
</view>
</view>
<!-- 已完成 -->
<view class="plan-completed" v-else>
<u-icon name="checkmark-circle-fill" size="48" color="#4caf50"></u-icon>
<text class="completed-label">分组完成</text>
<text class="completed-label" style="color: var(--nav-mian); font-size: 24rpx;"
@click="resetGroup(groupIndex)">重置</text>
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-controls">
<view class="controls-inner">
<view v-if="!isRunning && !isAllCompleted" class="control-btn start-btn" @click="startTraining">
<u-icon name="play-circle" size="32" color="#fff"></u-icon>
<text class="btn-text">开始</text>
</view>
<view v-else-if="isRunning" class="control-btn pause-btn" @click="pauseTraining">
<u-icon name="pause-circle" size="32" color="#fff"></u-icon>
<text class="btn-text">暂停</text>
</view>
<view class="control-btn reset-btn" @click="resetTraining">
<u-icon name="reload" size="32" color="#fff"></u-icon>
<text class="btn-text">重置</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import { Service } from '@/Service/Service'
import { PlanService } from '@/Service/swimming/PlanService'
import { onLoad, onShow } from '@dcloudio/uni-app'
// ==================== 类型 ====================
interface PlanItem {
name : string
target : number
rest : number
circle : number
}
interface GroupData {
groupName : string
allTime : string
group : PlanItem[]
}
interface GroupState {
groupName : string
plans : PlanItem[]
currentPlanIndex : number
currentPlan : PlanItem | null
countdown : number
restCountdown : number
completedLaps : number
isResting : boolean
isCompleted : boolean
isPlanCompleted : boolean // 当前计划是否已完成(最后一圈休息后切换计划)
isRunning : boolean // 当前分组是否正在运行
}
// ==================== 数据 ====================
const planId = ref('')
const name = ref('')
const groupStates = ref<GroupState[]>([])
const isRunning = ref(false)
const timer = ref<number | null>(null)
const planTotalDuration = ref(0) // 接口返回的计划总时长
const isLandscape = ref(false) // 是否为横屏
// ==================== 计算属性 ====================
const totalGroupPlans = computed(() => {
return groupStates.value.reduce((sum, gs) => sum + gs.plans.length, 0)
})
const totalCompletedGroupPlans = computed(() => {
return groupStates.value.reduce((sum, gs) => {
if (gs.isCompleted) return sum + gs.plans.length
return sum + gs.currentPlanIndex
}, 0)
})
const isAllCompleted = computed(() => {
return groupStates.value.length > 0 && groupStates.value.every(gs => gs.isCompleted)
})
const totalDuration = computed(() => {
return groupStates.value.reduce((total, gs) => {
return total + gs.plans.reduce((sum, plan) => {
return sum + plan.target * plan.circle + plan.rest * (plan.circle - 1)
}, 0)
}, 0)
})
// ==================== 生命周期 ====================
onLoad((data : any) => {
planId.value = data.id
getPlanInfo()
})
onShow(() => {
checkOrientation()
uni.onWindowResize(checkOrientation)
})
onShow(() => {
uni.$on('change',()=>{
getPlanInfo()
})
})
onUnmounted(() => {
stopTimer()
uni.offWindowResize(checkOrientation)
})
// ==================== 方法 ====================
const checkOrientation = () => {
const info = uni.getSystemInfoSync()
isLandscape.value = info.windowWidth > info.windowHeight
}
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code === 0) {
name.value = res.data.plan.name
const data : GroupData[] = JSON.parse(res.data.plan.project)
initGroups(data)
} else {
Service.Msg(res.msg)
}
}).catch(() => {
Service.Msg('获取计划信息失败')
})
}
const initGroups = (data : GroupData[]) => {
groupStates.value = data.map(g => ({
groupName: g.groupName,
allTime: g.allTime,
plans: g.group,
currentPlanIndex: 0,
currentPlan: g.group[0] || null,
countdown: 0,
restCountdown: 0,
completedLaps: 0,
isResting: false,
isCompleted: g.group.length === 0,
isPlanCompleted: false,
isRunning: false
}))
}
// ==================== 分组级控制 ====================
const startGroup = (gs : GroupState) => {
if (gs.isCompleted) {
Service.Msg('该分组已完成')
return
}
// 初始化未开始的分组
if (!gs.isCompleted && gs.countdown === 0 && !gs.isResting) {
gs.countdown = gs.currentPlan?.target || 0
}
gs.isRunning = true
// 如果全局计时器未运行,启动它
if (!timer.value) {
startGlobalTimer()
}
}
const pauseGroup = (gs : GroupState) => {
gs.isRunning = false
// 检查是否还有运行的分组,如果没有则停止全局计时器
const hasRunning = groupStates.value.some(g => g.isRunning)
if (!hasRunning) {
stopTimer()
}
}
const resetGroup = (groupIndex : number) => {
const gs = groupStates.value[groupIndex]
if (!gs) return
// 停止当前分组运行
gs.isRunning = false
// 重置分组状态
gs.currentPlanIndex = 0
gs.currentPlan = gs.plans[0] || null
gs.countdown = 0
gs.restCountdown = 0
gs.completedLaps = 0
gs.isResting = false
gs.isCompleted = gs.plans.length === 0
gs.isPlanCompleted = false
// 检查是否还有运行的分组,如果没有则停止全局计时器
const hasRunning = groupStates.value.some(g => g.isRunning)
if (!hasRunning) {
stopTimer()
}
}
// ==================== 全局计时器 ====================
const startGlobalTimer = () => {
isRunning.value = true
timer.value = setInterval(() => {
let hasRunningGroup = false
let allCompleted = true
groupStates.value.forEach(gs => {
if (gs.isCompleted) return
allCompleted = false
if (!gs.isRunning) return
hasRunningGroup = true
if (gs.isResting) {
// 休息倒计时
gs.restCountdown -= 0.01
if (gs.restCountdown <= 0) {
gs.isResting = false
gs.restCountdown = 0
if (gs.isPlanCompleted) {
// 当前计划所有圈已完成,切换到下一个计划
gs.isPlanCompleted = false
gs.currentPlanIndex++
if (gs.currentPlanIndex >= gs.plans.length) {
// 分组全部完成
gs.isCompleted = true
gs.currentPlan = null
gs.isRunning = false
} else {
// 开始下一个计划
gs.currentPlan = gs.plans[gs.currentPlanIndex]
gs.countdown = gs.currentPlan.target
gs.completedLaps = 0
}
} else {
// 继续当前计划的下一圈
gs.countdown = gs.currentPlan?.target || 0
}
}
} else {
// 训练倒计时
gs.countdown -= 0.01
if (gs.countdown <= 0) {
gs.completedLaps++
if (gs.completedLaps >= (gs.currentPlan?.circle || 0)) {
// 当前计划最后一圈完成,进入休息(休息后切换计划)
gs.isPlanCompleted = true
gs.isResting = true
gs.restCountdown = gs.currentPlan?.rest || 0
} else {
// 进入休息
gs.isResting = true
gs.restCountdown = gs.currentPlan?.rest || 0
}
}
}
})
if (allCompleted || !hasRunningGroup) {
stopTimer()
}
}, 10)
}
const startTraining = () => {
if (groupStates.value.length === 0) {
Service.Msg('暂无训练计划')
return
}
if (isAllCompleted.value) {
Service.Msg('训练已完成')
return
}
// 启动所有未完成的组
groupStates.value.forEach(gs => {
if (!gs.isCompleted) {
startGroup(gs)
}
})
}
const pauseTraining = () => {
groupStates.value.forEach(gs => {
gs.isRunning = false
})
stopTimer()
}
const stopTimer = () => {
isRunning.value = false
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
}
const resetTraining = () => {
stopTimer()
const data = groupStates.value.map(gs => ({
groupName: gs.groupName,
allTime: gs.allTime,
group: gs.plans
}))
initGroups(data)
}
const formatCountdown = (seconds : number) : string => {
const s = Math.max(0, Math.floor(seconds))
const m = Math.floor(s / 60)
const sec = s % 60
return `${m.toString().padStart(2, '0')}${sec.toString().padStart(2, '0')}`
}
const formatTotalDuration = (seconds : number) : string => {
if (seconds === 0) return '0分0秒'
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return `${m}${s}`
}
</script>
<style lang="scss" scoped>
.hunyang-container {
min-height: 100vh;
background-color: #f5f7fa;
padding: 20rpx;
padding-bottom: 200rpx;
}
.card-section {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.header-section {
position: relative;
.header-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.project-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.header-action {
position: absolute;
top: 24rpx;
right: 24rpx;
padding: 4rpx;
}
.total-duration {
display: flex;
align-items: center;
gap: 8rpx;
background: #f0f0f0;
padding: 10rpx 20rpx;
border-radius: 20rpx;
.duration-label {
font-size: 24rpx;
color: #666;
}
.duration-value {
font-size: 26rpx;
font-weight: 500;
color: #333;
}
}
.total-progress {
.progress-text {
font-size: 24rpx;
color: #999;
}
}
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
padding-left: 16rpx;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4rpx;
height: 24rpx;
background: #4a90e2;
border-radius: 2rpx;
}
}
.groups-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.group-card {
background: #f8f9fa;
border-radius: 12rpx;
padding: 24rpx;
border: 2rpx solid #e8e8e8;
transition: all 0.2s ease;
&.resting {
border-color: #ff9800;
background: #fffbf0;
}
&.completed {
border-color: #4caf50;
background: #f0f9f0;
opacity: 0.8;
}
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
gap: 4rpx;
}
.group-title-area {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.group-name {
font-size: 22rpx;
font-weight: 600;
color: #333;
width: 78rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.group-plan-index {
font-size: 22rpx;
color: #999;
}
.status-tag {
padding: 6rpx 16rpx;
border-radius: 8rpx;
background: #e6f0ff;
font-size: 22rpx;
color: #4a90e2;
font-weight: 500;
&.completed {
background: #e8f5e9;
color: #4caf50;
}
}
.plan-content {
text-align: center;
}
.plan-name {
font-size: 22rpx;
font-weight: 600;
color: #333;
}
/* 圈数指示器 */
.laps-indicator {
display: flex;
justify-content: center;
gap: 12rpx;
margin-bottom: 16rpx;
.lap-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #e0e0e0;
transition: all 0.3s ease;
&.completed {
background: #4caf50;
transform: scale(1.2);
}
}
}
/* 阶段提示 */
.phase-hint {
margin-bottom: 16rpx;
padding: 8rpx 20rpx;
border-radius: 20rpx;
background: #e6f0ff;
display: inline-block;
.phase-text {
font-size: 24rpx;
color: #4a90e2;
font-weight: 500;
}
&.resting {
background: #fff8e1;
.phase-text {
color: #ff9800;
}
}
}
.countdown-box {
.countdown-number {
font-size: 72rpx;
font-weight: bold;
color: #000;
}
&.resting .countdown-number {
color: red;
animation: flash 0.5s ease-in-out infinite;
}
}
.plan-details {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
}
.detail-badge {
display: inline-flex;
align-items: center;
gap: 4rpx;
font-size: 24rpx;
color: #666;
background: #f5f7fa;
padding: 8rpx 4rpx;
border-radius: 24rpx;
}
.detail-badge-tag {
width: 32rpx;
height: 32rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
border-radius: 50%;
font-size: 20rpx;
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 分组控制按钮 */
.group-controls {
display: flex;
justify-content: space-between;
gap: 20rpx;
padding-top: 16rpx;
}
.group-control-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 24rpx;
border-radius: 24rpx;
font-size: 24rpx;
color: #fff;
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
}
.group-start-btn {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
}
.group-pause-btn {
background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
}
.group-reset-btn {
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
}
.plan-completed {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx;
gap: 12rpx;
.completed-label {
font-size: 28rpx;
color: #4caf50;
font-weight: 500;
}
}
.all-completed {
.completed-center {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx;
gap: 16rpx;
}
.completed-title {
font-size: 36rpx;
font-weight: 600;
color: #4caf50;
}
.completed-desc {
font-size: 26rpx;
color: #999;
}
}
.bottom-controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
padding: 16rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
z-index: 100;
border-top: 1rpx solid #e8e8e8;
}
.controls-inner {
display: flex;
justify-content: center;
align-items: center;
gap: 20rpx;
max-width: 750rpx;
margin: 0 auto;
}
.control-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 20rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
flex: 1;
&:active {
transform: scale(0.98);
}
}
.btn-text {
font-size: 24rpx;
font-weight: 500;
color: #fff;
}
.start-btn {
background: #4caf50;
}
.pause-btn {
background: #ff9800;
}
.reset-btn {
background: #2196f3;
}
@keyframes flash {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
/* 横屏适配 */
@media (orientation: landscape) {
.hunyang-container {
padding: 16rpx;
padding-bottom: 140rpx;
}
.card-section {
padding: 16rpx 20rpx;
margin-bottom: 12rpx;
}
.header-section {
.header-content {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.header-action {
position: static;
}
.project-name {
font-size: 34rpx;
letter-spacing: 2rpx;
}
}
.section-title {
font-size: 26rpx;
margin-bottom: 12rpx;
letter-spacing: 1rpx;
}
.groups-list {
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx;
}
.group-card {
padding: 16rpx 20rpx;
flex: 1;
min-width: 40%;
max-width: 50%;
box-sizing: border-box;
}
.group-header {
margin-bottom: 10rpx;
}
.group-name {
font-size: 18rpx;
letter-spacing: 1rpx;
width: 78rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.group-plan-index {
font-size: 18rpx;
letter-spacing: 1rpx;
}
.plan-name {
font-size: 18rpx;
letter-spacing: 1rpx;
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
}
.laps-indicator {
margin-bottom: 10rpx;
gap: 10rpx;
.lap-dot {
width: 14rpx;
height: 14rpx;
}
}
.countdown-box {
margin: 10rpx 0;
.countdown-number {
font-size: 72rpx;
transform: scale(1.2, 1.3);
}
}
.phase-hint {
margin-bottom: 10rpx;
padding: 6rpx 16rpx;
.phase-text {
font-size: 22rpx;
letter-spacing: 1rpx;
}
}
.plan-details {
gap: 12rpx;
}
.detail-badge {
font-size: 24rpx;
padding: 6rpx 14rpx;
letter-spacing: 1rpx;
}
.group-controls {
padding-top: 10rpx;
gap: 16rpx;
justify-content: center;
}
.group-control-btn {
font-size: 24rpx;
padding: 10rpx 28rpx;
letter-spacing: 2rpx;
gap: 6rpx;
}
.plan-completed {
padding: 20rpx;
gap: 10rpx;
.completed-label {
font-size: 26rpx;
letter-spacing: 2rpx;
}
}
.bottom-controls {
padding: 10rpx 32rpx;
.controls-inner {
max-width: 100%;
gap: 24rpx;
justify-content: center;
}
}
.control-btn {
padding: 7rpx 32rpx;
flex: none;
min-width: 140rpx;
gap: 8rpx;
}
.btn-text {
font-size: 18rpx;
letter-spacing: 2rpx;
}
}
</style>

394
src/pages/userFunc/plan.vue Normal file
View File

@@ -0,0 +1,394 @@
<template>
<view class="plan-container">
<!-- 训练计划列表 -->
<view class="plan-list-section">
<view class="section-header">
<text class="section-title">我的训练计划</text>
<text class="plan-count">{{ trainingPlans.length }}个计划</text>
</view>
<!-- 计划列表 -->
<view class="plan-list">
<view v-for="(plan, index) in trainingPlans" :key="plan.id" class="plan-card">
<!-- 计划信息 -->
<view class="plan-info">
<view class="plan-icon">
<view class="icon-circle">
<u-icon name="calendar" size="24" color="#fff"></u-icon>
</view>
</view>
<view class="plan-detail">
<text class="plan-name">{{ plan.name }}</text>
<view class="plan-meta">
<view class="meta-tag">
<text class="tag-text">{{ plan.laps }}</text>
</view>
<view class="meta-tag">
<text class="tag-text">混氧</text>
</view>
</view>
</view>
</view>
<!-- 删除按钮 -->
<view class="delete-btn">
<u-icon name="trash" size="20" color="#ff4d4f"></u-icon>
</view>
</view>
<!-- 空状态 -->
<view v-if="trainingPlans.length === 0" class="empty-state">
<view class="empty-icon">
<u-icon name="calendar" size="80" color="#d9d9d9"></u-icon>
</view>
<text class="empty-text">暂无训练计划</text>
<text class="empty-desc">点击下方按钮添加第一个训练计划</text>
</view>
</view>
</view>
<!-- 底部添加计划按钮 -->
<view class="bottom-section">
<view class="add-plan-btn" @click="Service.GoPage('/pages/userFunc/addplan')">
<u-icon name="plus" size="24" color="#fff"></u-icon>
<text class="add-btn-text">添加训练计划</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Service } from '@/Service/Service'
interface TrainingPlan {
id : string
name : string
laps : string
}
const trainingPlans = ref<TrainingPlan[]>([
{
id: '1',
name: '自由泳500米基础训练',
laps: '10'
},
{
id: '2',
name: '蛙泳200米进阶训练',
laps: '4'
},
{
id: '3',
name: '混合泳1000米挑战',
laps: '20'
},
{
id: '4',
name: '蝶泳400米专项训练',
laps: '8'
}
])
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.plan-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 180rpx;
}
/* 计划列表区域 */
.plan-list-section {
padding: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding: 0 8rpx;
}
.section-title {
font-size: 34rpx;
font-weight: 700;
color: #333;
position: relative;
padding-left: 16rpx;
/* 标题左侧装饰条 */
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 32rpx;
background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
border-radius: 3rpx;
}
}
.plan-count {
font-size: 26rpx;
color: #1890ff;
font-weight: 600;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(24, 144, 255, 0.05) 100%);
padding: 8rpx 20rpx;
border-radius: 20rpx;
}
/* 计划列表 */
.plan-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 单个计划卡片 - 美化版 */
.plan-card {
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
border-radius: 28rpx;
padding: 28rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
/* 卡片背景装饰 */
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 120rpx;
height: 120rpx;
background: radial-gradient(circle, rgba(24, 144, 255, 0.08) 0%, transparent 70%);
pointer-events: none;
}
&:active {
transform: scale(0.98) translateY(2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
}
/* 左侧序号 */
.plan-index {
flex-shrink: 0;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
.index-text {
font-size: 26rpx;
font-weight: 700;
color: #fff;
}
/* 计划信息区域 */
.plan-info {
display: flex;
align-items: center;
gap: 20rpx;
flex: 1;
min-width: 0;
}
.plan-icon {
flex-shrink: 0;
}
.icon-circle {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(24, 144, 255, 0.25);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 8rpx;
right: 8rpx;
width: 16rpx;
height: 16rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
}
.plan-detail {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.plan-name {
font-size: 32rpx;
font-weight: 700;
color: #333;
line-height: 1.3;
background: linear-gradient(135deg, #333 0%, #666 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.plan-meta {
display: flex;
align-items: center;
gap: 12rpx;
}
.meta-tag {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(24, 144, 255, 0.05) 100%);
border-radius: 16rpx;
border: 1rpx solid rgba(24, 144, 255, 0.2);
}
.tag-text {
font-size: 24rpx;
color: #1890ff;
font-weight: 600;
}
/* 删除按钮 - 美化版 */
.delete-btn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
border: 2rpx solid rgba(255, 77, 79, 0.2);
&:active {
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
transform: scale(0.9);
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.4);
border-color: transparent;
u-icon {
color: #fff;
}
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140rpx 40rpx;
gap: 24rpx;
}
.empty-icon {
margin-bottom: 20rpx;
opacity: 0.6;
}
.empty-text {
font-size: 32rpx;
color: #8c8c8c;
font-weight: 600;
}
.empty-desc {
font-size: 26rpx;
color: #bfbfbf;
}
/* 底部添加计划按钮 */
.bottom-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, transparent 0%, rgba(245, 245, 245, 0.95) 20%, #f5f5f5 100%);
padding: 24rpx 20rpx 40rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
z-index: 100;
}
.add-plan-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
width: 100%;
height: 100rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #36cfc9 100%);
background-size: 200% 200%;
border-radius: 24rpx;
box-shadow: 0 12rpx 32rpx rgba(24, 144, 255, 0.35), 0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
/* 光泽效果 */
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: shimmer 3s infinite;
}
&:active {
transform: scale(0.96);
box-shadow: 0 6rpx 20rpx rgba(24, 144, 255, 0.25);
}
}
.add-btn-text {
font-size: 34rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
position: relative;
z-index: 1;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,435 @@
<template>
<view class="project-list-container">
<!-- 页面标题区域 -->
<view class="header-section">
<view class="header-title" style="display: flex; align-items: center; justify-content: space-between;">
<text class="title">项目列表</text>
<text class="subtitle">{{ projects.length }}个项目</text>
</view>
</view>
<!-- 项目列表 -->
<view class="projects-section">
<view v-if="projects.length === 0" class="empty-state">
<view class="empty-icon">
<u-icon name="list" size="64" color="#ddd"></u-icon>
</view>
<text class="empty-text">暂无项目</text>
<text class="empty-desc">点击下方按钮创建第一个项目</text>
</view>
<view v-else class="projects-list">
<view v-for="(project, index) in projects" :key="project.planId" class="project-card"
@click="viewProjectDetail(project)">
<view class="project-delete" @click.stop="deleteProject(project, index)">
<u-icon name="trash" size="18" color="#ff4d4f"></u-icon>
</view>
<view class="project-info">
<view class="project-title-row">
<text class="project-name">{{ project.name }}</text>
<view class="project-mode" :class="getModeClass(project.mode)">
<text class="mode-text">{{ project.mode }}</text>
</view>
</view>
<view class="project-stats">
<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>
<view class="project-footer">
<view class="">
</view>
<view class="project-time">
<u-icon name="clock" size="14" color="#999"></u-icon>
<text class="time-text">{{ Service.formatDate(project.addTime,1) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { PlanService } from '@/Service/swimming/PlanService'
import { ref } from "vue"
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 项目列表数据
const projects = ref<Array<any>>([])
onLoad(() => {
getData()
})
onReachBottom(() => {
getList()
})
// 获取模式对应的样式类
const getModeClass = (mode : string) : string => {
const classMap : Record<string, string> = {
'计时': 'mode-timing',
'包干': 'mode-package',
'分段': 'mode-segment'
}
return classMap[mode] || ''
}
// 查看项目详情
const viewProjectDetail = (project : any) => {
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)
}
}
// 删除项目
const deleteProject = (project : any, index : number) => {
Service.Confirm(`确定要删除「${project.name}」吗?`, () => {
PlanService.DeletePlan(project.planId).then(res => {
if (res.code == 0) {
getData()
Service.Msg('删除成功')
} else {
Service.Msg(res.msg)
}
})
})
}
// 获取项目列表数据
const getData = () => {
projects.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
// 获取项目列表
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
PlanService.GetPlanList( '', page.value.toString()).then(res => {
if (res.code == 0) {
projects.value = [...projects.value, ...res.data]
status.value = res.data.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
// 添加项目
const addProject = () => {
Service.GoPage('/pages/userFunc/setCourse')
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.project-list-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 180rpx;
}
/* 页面标题区域 */
.header-section {
margin-bottom: 24rpx;
.header-title {
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #999;
}
}
}
/* 项目列表区域 */
.projects-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.section-count {
font-size: 24rpx;
color: #999;
background-color: #f5f5f5;
padding: 6rpx 16rpx;
border-radius: 16rpx;
}
}
.empty-state {
background-color: #fff;
border-radius: 24rpx;
padding: 100rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.empty-icon {
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
font-size: 30rpx;
font-weight: 600;
color: #666;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: #999;
}
}
}
.projects-list {
.project-card {
background-color: #fff;
border-radius: 20rpx;
padding: 28rpx 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::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;
}
&:active {
transform: scale(0.98);
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.04);
&::before {
opacity: 1;
}
}
.project-delete {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: linear-gradient(135deg, #fff1f0 0%, #ffccc7 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79.3);
transition: all 0.3s ease;
z-index: 2;
&:active {
transform: scale(0.9);
background: linear-gradient(135deg, #ffccc7 0%, #ffa39e 100%);
}
}
.project-info {
padding-right: 80rpx;
.project-title-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
flex-wrap: wrap;
}
.project-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.project-mode {
padding: 6rpx 14rpx;
border-radius: 12rpx;
font-size: 22rpx;
font-weight: 600;
flex-shrink: 0;
&.mode-timing {
background-color: #e6f7ff;
color: #1890ff;
}
&.mode-package {
background-color: #fff7e6;
color: #faad14;
}
&.mode-segment {
background-color: #f6ffed;
color: #52c41a;
}
.mode-text {
font-size: 22rpx;
font-weight: 600;
}
}
.project-stats {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
.stat-badge {
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 14rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
&.record-badge {
background-color: #f6ffed;
}
.badge-text {
font-size: 22rpx;
color: #666;
font-weight: 500;
}
}
}
}
.project-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
.project-time {
display: flex;
align-items: center;
gap: 6rpx;
.time-text {
font-size: 22rpx;
color: #999;
}
}
.project-arrow {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.04);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
}
&:active .project-arrow {
transform: translateX(6rpx);
background-color: rgba(24, 144, 255, 0.1);
}
}
}
/* 底部添加按钮 */
.bottom-add-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100;
.add-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
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);
}
.btn-text {
font-size: 32rpx;
color: #fff;
font-weight: 700;
}
}
}
</style>

View File

@@ -0,0 +1,863 @@
<template>
<view class="segmentation-container">
<!-- 总计时器区域 -->
<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" style="margin-top: 20rpx;">
<!-- <view class="section-header">
<text class="section-title">学生列表</text>
<text class="student-count">{{ students.length }}位学生</text>
</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" @click="showStudentRecords(student)"
:class="{ 'started': student.hasStarted }">{{ student.number }}
</view>
<view class="student-info">
<text class="student-name">{{ student.name }}</text>
<!-- <view class="start-status" v-if="startMode === 1 && !student.hasStarted">
<text class="status-text">等待</text>
</view> -->
<text class="total-time-text"
v-if="student.segments.length > 0">{{ formatSimpleTime(getLastSegmentTime(student)) }}</text>
<view class="record-badge"
:class="{ 'completed': student.segments.length >= Number(segmentCount) }">
<text class="badge-text">{{ student.segments.length }}/{{ segmentCount }}</text>
</view>
</view>
<view class="student-buttons">
<button class="record-btn" :disabled="!student.hasStarted"
@click="recordStudentSegment(student)">
<text class="btn-text">记录</text>
</button>
<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> -->
</view>
</view>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-btn reset-action" @click="resetAll">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="action-text">重置</text>
</view>
<view class="action-btn timer-action" :class="{ 'running': isRunning }" @click="toggleTimer">
<u-icon :name="isRunning ? 'pause-circle' : 'play-circle'" size="28" color="#fff"></u-icon>
<text class="action-text">{{ isRunning ? '暂停' : '开始' }}</text>
</view>
<view class="action-btn save-action" @click="saveData">
<u-icon name="checkmark" size="24" color="#fff"></u-icon>
<text class="action-text">保存</text>
</view>
</view>
<!-- 记录弹窗 -->
<u-popup :show="showRecordPopup" mode="bottom" :round="20" @close="closeRecordPopup">
<view class="record-popup">
<view class="popup-header">
<text class="popup-title">{{ currentStudent?.name }} - 分段记录</text>
<view class="close-btn" @click="closeRecordPopup">
<u-icon name="close" size="24" color="#999"></u-icon>
</view>
</view>
<view class="popup-summary">
<view class="summary-item">
<text class="summary-label">总距离</text>
<text class="summary-value">{{ totalDistance }}</text>
</view>
<view class="summary-item">
<text class="summary-label">已记录</text>
<text
class="summary-value">{{ currentStudent?.segments?.length || 0 }}/{{ segmentCount }}</text>
</view>
</view>
<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>
<text class="record-cell header-cell">距离</text>
<text class="record-cell header-cell">累计时间</text>
<text class="record-cell header-cell">分段用时</text>
</view>
<view v-for="(segment, index) in currentStudent.segments" :key="index" class="record-item">
<text class="record-cell">{{ index + 1 }}</text>
<text class="record-cell">{{ segmentDistance }}</text>
<text class="record-cell time-cell">{{ formatTime(segment.time) }}</text>
<text class="record-cell time-cell">{{ formatTimeDiff(index, segment.time) }}</text>
</view>
</scroll-view>
<view class="empty-record" v-else>
<u-icon name="info-circle" size="48" color="#ccc"></u-icon>
<text class="empty-text">暂无记录数据</text>
</view>
<view class="popup-footer">
<button class="popup-btn close-popup" @click="closeRecordPopup">关闭</button>
</view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
import { ref, onUnmounted, computed } from 'vue'
import { Service } from '@/Service/Service'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { PlanService } from '@/Service/swimming/PlanService'
// 分段配置
const segmentDistance = ref(50)
const segmentCount = ref(4)
// 出发模式: 0-一起出发, 1-间隔出发
const startMode = ref(0)
// 间隔时间(秒)
const intervalTime = ref(10)
// 计算总距离
const totalDistance = computed(() => {
return Number(segmentDistance.value) * Number(segmentCount.value)
})
// 学生列表
const students = ref<Array<any>>([])
// 弹窗状态
const showRecordPopup = ref(false)
const currentStudent = ref<any>(null)
// 计时器状态
const isRunning = ref(false)
const currentTime = ref(0)
let timerInterval : number | null = null
let startTime : number = 0
let intervalStartTimer : number | null = null
let planId = ref('')
onLoad((data : any) => {
planId.value = data.id
})
onShow(() => {
getPlanInfo()
})
// 获取计划详情
const getPlanInfo = () => {
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
// 将计划数据转换为选手数据
// athletes.value = res.data.plan.users.
students.value = res.data.plan.users.map((item : any, index : any) => {
return {
id: item.studentId,
number: index + 1,
name: item.name,
segments: [],
hasStarted: res.data.plan.departType == '间隔出发' ? false : true,
startTime: 0
}
})
startMode.value = res.data.plan.departType == '间隔出发' ? 1 : 0
intervalTime.value = res.data.plan.interval
} else {
Service.Msg(res.msg)
}
})
}
// 格式化时间显示(分:秒.毫秒)
const formatTime = (seconds : number) : string => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${millis.toString().padStart(2, '0')}`
}
// 格式化持续时间
const formatDuration = (seconds : number) : string => {
const hours = Math.floor(seconds / 3600)
const mins = Math.floor((seconds % 3600) / 60)
let result = ''
if (hours > 0) {
result += `${hours}小时`
}
if (mins > 0) {
result += `${mins}分钟`
}
if (hours === 0 && mins === 0) {
result = '0分钟'
}
return result
}
// 格式化时间差(详情弹窗用)
// 第一段显示累计时间,后续段显示分段用时
const formatTimeDiff = (index : number, currentTime : number) : string => {
if (index === 0) {
// 第一段显示累计时间
return formatTime(currentTime)
}
if (!currentStudent.value || !currentStudent.value.segments[index - 1]) {
return '00:00:00'
}
const prevTime = currentStudent.value.segments[index - 1].time
const diff = currentTime - prevTime
return formatTime(diff)
}
// 获取学生最后一次记录的分段用时
// 第一段返回累计时间,后续段返回当前段用时(当前累计 - 上一次累计)
const getLastSegmentTime = (student : any) : number => {
if (!student.segments || student.segments.length === 0) {
return 0
}
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)
const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${millis.toString().padStart(2, '0')}`
}
// 开始计时
const startTimer = () => {
if (isRunning.value) return
isRunning.value = true
startTime = Date.now() - currentTime.value * 1000
timerInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000
currentTime.value = elapsed
}, 1)
// 间隔出发模式
if (startMode.value === 1) {
// 第一个学生立即出发
if (students.value.length > 0) {
students.value[0].hasStarted = true
students.value[0].startTime = 0
}
// 设置间隔出发定时器
const interval = intervalTime.value * 1000
let startedCount = 1
intervalStartTimer = setInterval(() => {
if (startedCount < students.value.length) {
const student = students.value[startedCount]
student.hasStarted = true
student.startTime = (Date.now() - startTime) / 1000
startedCount++
Service.Msg(`${student.name} 已出发`)
} else {
clearInterval(intervalStartTimer as number)
intervalStartTimer = null
}
}, interval)
}
}
// 停止计时
const stopTimer = () => {
if (!isRunning.value) return
isRunning.value = false
if (timerInterval) {
clearInterval(timerInterval)
timerInterval = null
}
if (intervalStartTimer) {
clearInterval(intervalStartTimer)
intervalStartTimer = null
}
}
// 切换计时器状态
const toggleTimer = () => {
if (isRunning.value) {
stopTimer()
} else {
startTimer()
}
}
// 记录学生分段
const recordStudentSegment = (student : any) => {
if (!isRunning.value) {
Service.Msg('请先开始计时')
return
}
if (!student.hasStarted) {
Service.Msg('该学生尚未出发')
return
}
const maxSeg = Number(segmentCount.value)
if (student.segments.length >= maxSeg) {
Service.Msg(`已达到最大分段数(${maxSeg}段)`)
return
}
let recordTime = currentTime.value
// 间隔出发模式下,使用学生自己的开始时间
if (startMode.value === 1) {
recordTime = currentTime.value - student.startTime
}
student.segments.push({
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('所有学生记录完成')
}
}
// 重置学生
const resetStudent = (student : any) => {
student.segments = []
student.hasStarted = false
student.startTime = 0
Service.Msg(`${student.name} 已重置`)
}
// 重置全部
const resetAll = () => {
stopTimer()
currentTime.value = 0
students.value.forEach(student => {
student.segments = []
student.hasStarted = startMode.value == 1 ? false : true,
student.startTime = 0
})
Service.Msg('已全部重置')
}
// 保存数据
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('暂无数据可保存!')
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('用户点击取消');
}
}
});
}
// 显示学生记录弹窗
const showStudentRecords = (student : any) => {
currentStudent.value = student
showRecordPopup.value = true
}
// 关闭记录弹窗
const closeRecordPopup = () => {
showRecordPopup.value = false
currentStudent.value = null
}
// 页面卸载时清理计时器
onUnmounted(() => {
if (timerInterval) {
clearInterval(timerInterval)
}
if (intervalStartTimer) {
clearInterval(intervalStartTimer)
}
})
</script>
<style lang="scss" scoped>
.segmentation-container {
min-height: 100vh;
background-color: #f6f6f6;
padding: 20rpx 20rpx;
padding-bottom: 180rpx;
}
/* 总计时器区域 */
.total-time-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.config-info {
margin-top: 10rpx;
background-color: #e6f7ff;
border-radius: 8rpx;
padding: 16rpx 20rpx;
text-align: center;
.info-text {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
}
}
.timer-bar {
display: flex;
align-items: center;
justify-content: center;
.timer-time {
font-size: 80rpx;
font-weight: bold;
color: #ff4d4f;
font-family: 'DIN Alternate', monospace;
}
}
/* 学生列表区域 */
.students-section {
margin-bottom: 20rpx;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.student-count {
font-size: 24rpx;
color: #999;
background-color: #f5f5f5;
padding: 6rpx 16rpx;
border-radius: 16rpx;
}
}
.students-list {
.student-item {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
&.not-started {
opacity: 0.6;
}
.student-header {
display: flex;
align-items: center;
.student-number {
width: 56rpx;
height: 56rpx;
background-color: #e6f7ff;
color: #1890ff;
font-size: 28rpx;
font-weight: 600;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
&.started {
background-color: #52c41a;
color: #fff;
}
}
.student-info {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
.student-name {
width: 120rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.start-status {
background-color: #faad14;
border-radius: 8rpx;
padding: 4rpx 12rpx;
.status-text {
font-size: 22rpx;
color: #fff;
font-weight: 500;
}
}
.total-time-text {
font-size: 30rpx;
color: #999;
font-family: 'DIN Alternate', monospace;
font-weight: 500;
}
.record-badge {
background-color: #f0f0f0;
border-radius: 12rpx;
padding: 4rpx 12rpx;
.badge-text {
font-size: 22rpx;
color: #999;
font-weight: 500;
}
&.completed {
background-color: #52c41a;
.badge-text {
color: #fff;
}
}
}
}
.student-buttons {
display: flex;
gap: 12rpx;
.record-btn,
.view-btn,
.reset-btn {
height: 56rpx;
padding: 0 35rpx;
border-radius: 8rpx;
border: none;
font-size: 24rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 6rpx;
.btn-text {
font-size: 24rpx;
color: #fff;
}
}
.record-btn {
background-color: #52c41a;
&:disabled {
background-color: #d9d9d9;
opacity: 0.6;
}
}
.view-btn {
background-color: #1890ff;
}
.reset-btn {
background-color: #faad14;
}
}
}
}
}
}
/* 记录弹窗 */
.record-popup {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
height: 70vh;
overflow: hidden;
display: flex;
flex-direction: column;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.close-btn {
padding: 10rpx;
cursor: pointer;
}
}
.popup-summary {
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
background-color: #f6f6f6;
.summary-item {
flex: 1;
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
text-align: center;
.summary-label {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.summary-value {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
}
.record-list {
flex: 1;
overflow-y: auto;
padding: 0 30rpx 20rpx;
.record-header {
display: flex;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-bottom: 16rpx;
margin-top: 20rpx;
.record-cell {
flex: 1;
text-align: center;
font-size: 26rpx;
padding: 16rpx 8rpx;
color: #666;
}
.header-cell {
font-weight: 600;
color: #333;
}
}
.record-item {
display: flex;
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 12rpx;
border: 1rpx solid #f0f0f0;
.record-cell {
flex: 1;
text-align: center;
font-size: 26rpx;
padding: 20rpx 8rpx;
color: #333;
&.time-cell {
font-family: 'DIN Alternate', monospace;
font-weight: 600;
color: #1890ff;
}
}
}
}
.empty-record {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
}
.popup-footer {
padding: 20rpx 30rpx 40rpx;
border-top: 1rpx solid #f0f0f0;
.popup-btn {
height: 80rpx;
border-radius: 12rpx;
border: none;
font-size: 30rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.close-popup {
background-color: #f5f5f5;
color: #666;
}
}
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100;
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
cursor: pointer;
transition: all 0.2s ease;
.action-text {
font-size: 28rpx;
color: #fff;
font-weight: 500;
}
&:active {
transform: scale(0.98);
}
}
.reset-action {
background-color: #faad14;
}
.timer-action {
background-color: #52c41a;
flex: 1.5;
&.running {
background-color: #ff4d4f;
}
}
.save-action {
background-color: #1890ff;
}
}
</style>

326
src/pages/userFunc/set.vue Normal file
View File

@@ -0,0 +1,326 @@
<template>
<view class="settings-container">
<!-- 头像设置区域 -->
<view class="settings-card">
<view class="card-header">
<text class="card-title">头像设置</text>
</view>
<view class="avatar-section">
<view class="avatar-wrapper">
<view class="avatar-circle">
<img :src="Service.GetMateUrlByImg(userInfo.headImg)" alt="" style="width: 100%; height: 100%;" />
</view>
<view class="avatar-edit" @click="changeAvatar">
<u-icon name="camera" size="20" color="#fff"></u-icon>
</view>
</view>
<text class="avatar-tip">点击更换头像</text>
</view>
</view>
<!-- 基本信息设置 -->
<view class="settings-card">
<view class="card-header">
<text class="card-title">基本信息</text>
</view>
<view class="form-section">
<view class="form-group">
<text class="form-label">用户姓名</text>
<view class="input-wrapper">
<input
class="form-input"
v-model="userInfo.name"
placeholder="请输入用户姓名"
type="text"
maxlength="20"
/>
</view>
</view>
<view class="form-divider"></view>
<view class="form-group">
<text class="form-label">手机号码</text>
<view class="input-wrapper">
<input
class="form-input"
v-model="userInfo.phone"
placeholder="请输入手机号码"
type="number"
maxlength="11"
/>
</view>
</view>
</view>
</view>
<!-- 保存按钮 -->
<view class="save-section">
<button class="save-btn" @click="saveUserInfo">
<text class="btn-text">保存修改</text>
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { ref } from 'vue'
import { userService } from '@/Service/swimming/userService'
const userInfo = ref<any>({
headImg:'',
name: '',
phone: ''
})
onLoad(() => {
loadUserInfo()
})
onShow(() => {
})
const loadUserInfo = () => {
userService.GetUserInfo().then((content) => {
if (content.code == 0) {
userInfo.value=content.data.userInfo
} else {
Service.Msg(content.msg)
}
})
}
// 更换头像
const changeAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
let path = res.tempFiles[0].path
Service.uploadH5(path, 'Avatar', data => {
userInfo.value.headImg = data
})
}
})
}
// 保存用户信息
const saveUserInfo = () => {
if (!userInfo.value.name.trim()) {
Service.Msg('请输入用户姓名')
return
}
if (!userInfo.value.phone.trim()) {
Service.Msg('请输入手机号码')
return
}
// 简单的手机号验证
const phoneReg = /^1[3-9]\d{9}$/
if (!phoneReg.test(userInfo.value.phone)) {
Service.Msg('请输入正确的手机号码')
return
}
// 调用UpdateUserInfo接口
userService.UpdateUserInfo(userInfo.value.name, userInfo.value.phone, userInfo.value.headImg).then((content) => {
if (content.code == 0) {
Service.Msg('保存成功', 'success')
setTimeout(() => {
Service.GoPageBack()
}, 1000)
} else {
Service.Msg(content.msg)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.settings-container {
padding: 20rpx;
padding-bottom: 180rpx;
}
/* 页面标题区域 */
.header-section {
margin-bottom: 24rpx;
.header-title {
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #999;
}
}
}
/* 设置卡片 */
.settings-card {
background-color: #fff;
border-radius: 24rpx;
padding: 0;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
overflow: hidden;
.card-header {
padding: 24rpx 24rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
.card-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
}
/* 头像设置区域 */
.avatar-section {
padding: 40rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
.avatar-wrapper {
position: relative;
margin-bottom: 20rpx;
.avatar-circle {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 50%, #096dd9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3);
overflow: hidden;
}
.avatar-edit {
position: absolute;
bottom: 0;
right: 0;
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.4);
transition: all 0.3s ease;
&:active {
transform: scale(0.9);
}
}
}
.avatar-tip {
font-size: 24rpx;
color: #999;
}
}
/* 表单区域 */
.form-section {
padding: 20rpx 24rpx;
.form-group {
padding: 20rpx 0;
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.input-wrapper {
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 0 24rpx;
transition: all 0.2s ease;
&:focus-within {
background-color: #fff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.15);
}
.form-input {
width: 100%;
height: 88rpx;
font-size: 28rpx;
color: #333;
}
}
}
.form-divider {
height: 1rpx;
background-color: #f0f0f0;
margin: 0;
}
}
/* 保存按钮区域 */
.save-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100;
.save-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
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);
}
.btn-text {
font-size: 32rpx;
color: #fff;
font-weight: 700;
}
}
}
</style>

View File

@@ -0,0 +1,938 @@
<template>
<view class="course-container">
<!-- 表单区域 -->
<view class="form-section">
<!-- 项目名称 -->
<view class="form-card">
<view class="form-title">项目信息</view>
<view class="form-group">
<text class="form-label">项目名称</text>
<input class="form-input" :disabled="planId" v-model="courseData.projectName" placeholder="请输入项目名称"
placeholder-class="input-placeholder" />
</view>
</view>
<!-- 出发方式 -->
<view class="form-card">
<view class="form-title">出发方式</view>
<view class="radio-group">
<view class="radio-item" :class="{ active: courseData.startType === 'together' }"
@click="courseData.startType = 'together'">
<view class="radio-icon">
<view v-if="courseData.startType === 'together'" class="radio-inner"></view>
</view>
<text>一起出发</text>
</view>
<view class="radio-item" :class="{ active: courseData.startType === 'interval' }"
@click="courseData.startType = 'interval'">
<view class="radio-icon">
<view v-if="courseData.startType === 'interval'" class="radio-inner"></view>
</view>
<text>间隔出发</text>
</view>
</view>
<view v-if="courseData.startType === 'interval'" class="interval-input-wrapper">
<text class="interval-label">间隔时间</text>
<view class="interval-input">
<input class="number-input" v-model="courseData.intervalSeconds" type="digit"
placeholder="请输入秒数" />
<text class="unit-text"></text>
</view>
</view>
</view>
<!-- 组别设置 -->
<view v-if="type==1" class="form-card">
<view class="form-title">组别设置</view>
<view class="group-options">
<view class="person-input-wrapper">
<text class="person-label">每组人数</text>
<view class="person-input">
<input class="number-input" v-model="courseData.groupPersonCount" type="number"
placeholder="请输入人数" />
<text class="unit-text"></text>
</view>
</view>
</view>
</view>
<!-- 分段设置 -->
<view v-if="type==2" class="form-card">
<view class="form-title">分段设置</view>
<view class="segment-options">
<view class="segment-input-wrapper">
<text class="segment-label">分段距离</text>
<view class="segment-input">
<input class="number-input" v-model="courseData.segmentDistance" type="number"
placeholder="请输入距离" />
<text class="unit-text"></text>
</view>
</view>
<view class="segment-count-wrapper">
<text class="segment-count-label">分段数</text>
<view class="segment-count-input">
<input class="number-input" v-model="courseData.segmentCount" type="number"
placeholder="请输入段数" />
<text class="unit-text"></text>
</view>
</view>
</view>
</view>
<!-- 学生列表 -->
<view class="form-card">
<view class="student-header">
<view class="header-left">
<text class="form-title" style="margin-bottom: 0;">选择学生</text>
<text class="student-count">已选({{ selectedStudentIds.length }}/{{ allStudents.length }})</text>
</view>
<view class="header-actions">
<view class="select-all-btn" @click="toggleSelectAll">
<view class="checkbox-icon" :class="{ checked: allSelected }">
<u-icon v-if="allSelected" name="checkmark" size="14" color="#fff"></u-icon>
</view>
<text class="select-all-text">{{ allSelected ? '取消全选' : '全选' }}</text>
</view>
</view>
</view>
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="allStudents.length === 0" class="empty-student-state">
<view class="empty-icon">
<u-icon name="account" size="48" color="#ddd"></u-icon>
</view>
<text class="empty-text">暂无学生</text>
<text class="empty-desc">请先在学员管理中添加学生</text>
</view>
<view v-else class="student-list">
<view v-for="student in allStudents" :key="student.studentId" class="student-item"
:class="{ checked: selectedStudentIds.includes(student.studentId) }"
@click="toggleStudentSelect(student.studentId)">
<view class="student-checkbox"
:class="{ checked: selectedStudentIds.includes(student.studentId) }">
<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 v-if="planId && type==1 " class="student-time" >
pb:{{ student.quicklyTime}}
</view>
</view>
</view>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 80rpx;">
</view>
<!-- 底部按钮区域 -->
<view class="bottom-actions">
<view class="action-buttons">
<button class="confirm-btn" @click="confirmCreate()">保存项目</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Service } from '@/Service/Service'
import { onLoad } from '@dcloudio/uni-app'
import { PlanService } from '@/Service/swimming/PlanService'
import { studentService } from '@/Service/swimming/studentService'
// 课程数据
const courseData = ref({
projectName: '',
startType: 'together', // together | interval
intervalSeconds: '',
groupPersonCount: '',
segmentDistance: '',
segmentCount: ''
})
// 所有学生列表
const allStudents = ref<Array<any>>([])
// 已选学生ID列表
const selectedStudentIds = ref<string[]>([])
// 加载状态
const loading = ref(false)
let type = ref('')
let planId=ref('')
onLoad((data : any) => {
type.value = data.type
if(data.id){
planId.value=data.id
getPlanInfo()
}
getStudentList()
})
// 获取计划详情
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
selectedStudentIds.value=res.data.plan.users.map((item:any)=>{
return item.studentId
})
courseData.value.groupPersonCount=res.data.plan.groupInt
courseData.value.segmentDistance=res.data.plan.subsectionDistance
courseData.value.segmentCount=res.data.plan.subsectionInt
courseData.value.projectName=res.data.plan.name
courseData.value.startType = res.data.plan.departType == '间隔出发' ? 'interval' : 'together'
courseData.value.intervalSeconds = res.data.plan.interval
} else {
Service.Msg(res.msg)
}
})
}
// 是否全选
const allSelected = computed(() => {
return allStudents.value.length > 0 && allStudents.value.every(s => selectedStudentIds.value.includes(s.studentId))
})
// 已选学生列表(按选择顺序排序)
const selectedStudents = computed(() => {
return selectedStudentIds.value
.map(id => allStudents.value.find(s => s.studentId === id))
.filter((s) : s is Student => s !== undefined)
})
// 获取学生列表(模拟接口)
const getStudentList = async () => {
loading.value = true
studentService.GetStudentList(planId.value ).then(res => {
if (res.code == 0) {
allStudents.value = res.data
loading.value = false
} else {
Service.Msg(res.msg)
}
})
}
// 切换学生选中状态
const toggleStudentSelect = (id : string) => {
const index = selectedStudentIds.value.indexOf(id)
if (index > -1) {
selectedStudentIds.value.splice(index, 1)
} else {
selectedStudentIds.value.push(id)
}
}
// 全选/取消全选
const toggleSelectAll = () => {
if (allSelected.value) {
// 取消全选
selectedStudentIds.value = []
} else {
// 全选
selectedStudentIds.value = allStudents.value.map(s => s.studentId)
}
}
// 移除已选学生
const removeSelectedStudent = (id : string) => {
const index = selectedStudentIds.value.indexOf(id)
if (index > -1) {
selectedStudentIds.value.splice(index, 1)
}
}
// 保存项目
const confirmCreate = () => {
if (!courseData.value.projectName) {
Service.Msg('请输入项目名称!')
return
}
if (courseData.value.startType == 'interval' && !courseData.value.intervalSeconds) {
Service.Msg('请输入正确时间间隔!')
return
}
if (!courseData.value.groupPersonCount && type.value=='1') {
Service.Msg('请输入每组人数!')
return
}
if (!courseData.value.segmentDistance && type.value=='2') {
Service.Msg('请输入分段距离')
return
}
if (!courseData.value.segmentCount && type.value=='2') {
Service.Msg('请输入分段数')
return
}
let users=[]
selectedStudents.value.map((item:any)=>{
users.push({
studentId:item.studentId,
name:item.name
})
})
const data = {
planId: planId.value,
name: courseData.value.projectName,
planType:type.value=='1'?'计时项目':'分段项目',
departType: courseData.value.startType=='together'?'一起出发':'间隔出发',
interval: courseData.value.startType=='together'?0:courseData.value.intervalSeconds,
groupInt: courseData.value.groupPersonCount?courseData.value.groupPersonCount:'',
subsectionDistance: courseData.value.segmentDistance?courseData.value.segmentDistance:'',
subsectionInt: courseData.value.segmentCount?courseData.value.segmentCount:'',
users: JSON.stringify(users),
project: '',
group: '',
}
console.log(data);
PlanService.AddPlan(data).then(res=>{
if(res.code==0){
Service.Msg( planId.value?'修改成功!': '添加成功!')
if(planId.value){
Service.GoPageBack()
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)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.course-container {
min-height: 100vh;
padding-bottom: 140rpx;
}
/* 表单区域 */
.form-section {
padding: 20rpx;
}
.form-card {
background-color: #fff;
border-radius: 24rpx;
padding: 32rpx 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.form-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-bottom: 28rpx;
display: block;
}
}
/* 表单输入 */
.form-group {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 14rpx;
}
.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);
}
}
}
/* 单选按钮组 */
.radio-group {
display: flex;
gap: 20rpx;
margin-bottom: 24rpx;
.radio-item {
flex: 1;
height: 88rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: 28rpx;
color: #666;
transition: all 0.25s ease;
border: 2rpx solid transparent;
&:active {
transform: scale(0.96);
}
&.active {
font-weight: 600;
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
.radio-icon {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 3rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
&.active .radio-icon {
border-color: #1890ff;
background-color: #1890ff;
}
.radio-inner {
width: 12rpx;
height: 12rpx;
background-color: #fff;
border-radius: 50%;
}
}
}
/* 间隔时间输入 */
.interval-input-wrapper {
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.interval-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.interval-input {
flex: 1;
margin-left: 24rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
height: 72rpx;
.number-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.unit-text {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
}
/* 组别选项 */
.group-options {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.group-input-wrapper,
.person-input-wrapper {
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.group-label,
.person-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.group-input,
.person-input {
flex: 1;
margin-left: 24rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
height: 72rpx;
.number-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.unit-text {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
/* 分段设置选项 */
.segment-options {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.segment-input-wrapper,
.segment-count-wrapper {
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.segment-label,
.segment-count-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.segment-input,
.segment-count-input {
flex: 1;
margin-left: 24rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
height: 72rpx;
.number-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.unit-text {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
/* 学生列表头部 */
.student-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
.header-left {
display: flex;
align-items: center;
gap: 8rpx;
.student-count {
font-size: 26rpx;
color: #999;
font-weight: 500;
}
}
.header-actions {
.select-all-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
}
.checkbox-icon {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
&.checked {
color: #1890ff;
}
}
.select-all-text {
font-size: 24rpx;
color: #666;
}
}
}
}
/* 加载状态 */
.loading-state {
padding: 80rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top-color: #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
.loading-text {
font-size: 26rpx;
color: #999;
}
}
/* 空状态 */
.empty-student-state {
padding: 60rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.empty-icon {
margin-bottom: 20rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
font-weight: 600;
color: #666;
margin-bottom: 10rpx;
}
.empty-desc {
font-size: 24rpx;
color: #999;
}
}
/* 学生列表 */
.student-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.student-item {
width: calc((100% - 24rpx) / 3);
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 8rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
border: 2rpx solid transparent;
transition: all 0.25s ease;
&:active {
transform: scale(0.98);
}
&.checked {
background-color: #e6f7ff;
border-color: #1890ff;
}
.student-checkbox {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
&.checked {
border-color: #1890ff;
background-color: #1890ff;
}
.checkbox-index {
font-size: 22rpx;
color: #fff;
font-weight: 600;
line-height: 1;
}
}
.student-avatar {
width: 64rpx;
height: 64rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.avatar-text {
color: #fff;
font-size: 28rpx;
font-weight: 700;
}
}
.student-info {
width: 100%;
.student-name {
font-size: 26rpx;
font-weight: 600;
color: #333;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.student-time{
font-size: 24rpx;
color: #888;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.student-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
.gender-badge {
font-size: 22rpx;
padding: 4rpx 10rpx;
border-radius: 8rpx;
font-weight: 500;
&.male {
color: #1890ff;
background-color: #e6f7ff;
}
&.female {
color: #fa8c16;
background-color: #fff7e6;
}
}
.age-text {
font-size: 22rpx;
color: #999;
}
}
}
}
}
/* 已选学生预览 */
.selected-preview {
margin-top: 32rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f0f0f0;
.preview-header {
margin-bottom: 16rpx;
.preview-title {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
}
.preview-list {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
}
.preview-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 16rpx;
background-color: #e6f7ff;
border-radius: 12rpx;
.preview-index {
width: 24rpx;
height: 24rpx;
background-color: #1890ff;
color: #fff;
font-size: 18rpx;
font-weight: 600;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-name {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
.preview-remove {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:active {
transform: scale(0.9);
}
}
}
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 99;
.action-buttons {
display: flex;
gap: 16rpx;
}
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
border: none;
font-size: 30rpx;
font-weight: 600;
transition: all 0.25s ease;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
&:active {
background-color: #e8e8e8;
transform: scale(0.96);
}
}
.confirm-btn {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
&:active {
transform: scale(0.96);
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.25);
}
}
/* 动画 */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,860 @@
<template>
<view class="student-container">
<!-- 页面标题区域 -->
<view class="header-section">
<view class="header-title">
<text class="title">学员管理</text>
<text class="subtitle">{{ students.length }}位学员</text>
</view>
</view>
<!-- 学员列表 -->
<view class="students-section">
<view v-if="students.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>
<text class="empty-desc">点击下方按钮添加第一位学员</text>
</view>
<view v-else class="students-list">
<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" 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">
<text class="detail-label">出生日期</text>
<text class="detail-value">{{ student.birthday }}</text>
</view>
<view class="detail-row">
<text class="detail-label">年龄</text>
<text class="detail-value">{{ calculateAge(student.birthday) }}</text>
</view>
<view v-if="student.school" class="detail-row">
<text class="detail-label">学校</text>
<text class="detail-value">{{ student.school }}</text>
</view>
<view v-if="student.address" class="detail-row">
<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>
<!-- 底部添加按钮 -->
<view class="bottom-add-btn">
<button class="add-btn" @click="showAddModal = true">
<u-icon name="plus" size="24" color="#fff"></u-icon>
<text class="btn-text">添加学员</text>
</button>
</view>
<!-- 添加/编辑学员弹窗 -->
<view v-if="showAddModal || showEditModal" class="modal-overlay" @click="closeModal"></view>
<view v-if="showAddModal || showEditModal" class="student-modal">
<view class="modal-header">
<text class="modal-title">{{ showAddModal ? '添加学员' : '编辑学员' }}</text>
<view class="modal-close" @click="closeModal">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<view class="modal-content">
<view class="form-group">
<text class="form-label">学员姓名</text>
<input class="form-input" v-model="formData.name" placeholder="请输入学员姓名" type="text" />
</view>
<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-icon-box">
<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-icon-box">
<u-icon name="woman" size="20"
:color="formData.gender === '女' ? '#fff' : '#fa8c16'"></u-icon>
</view>
<text></text>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">出生日期</text>
<view class="date-picker-wrapper">
<picker mode="date" :value="formData.birthDate" @change="onBirthDateChange">
<view class="form-input date-picker" :class="{ placeholder: !formData.birthDate }">
<u-icon name="calendar" size="18" color="#1890ff"></u-icon>
<text>{{ formData.birthDate || '请选择出生日期' }}</text>
<u-icon name="arrow-down" size="14" color="#999"></u-icon>
</view>
</picker>
</view>
<view v-if="formData.birthDate" class="age-display">
<view class="age-icon">
<u-icon name="account" size="24" color="#fff"></u-icon>
</view>
<view class="age-info">
<text class="age-label">年龄</text>
<text class="age-value">{{ calculateAge(formData.birthDate) }}</text>
<text class="age-unit"></text>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">学校选填</text>
<input class="form-input" v-model="formData.school" placeholder="请输入学校名称" type="text" />
</view>
<view class="form-group">
<text class="form-label">家庭住址选填</text>
<input class="form-input" v-model="formData.address" placeholder="请输入家庭住址" type="text" />
</view>
</view>
<view class="modal-footer">
<button class="modal-cancel-btn" @click="closeModal">取消</button>
<button class="modal-confirm-btn" @click="saveStudent">
{{ showAddModal ? '添加' : '保存' }}
</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { studentService } from '@/Service/swimming/studentService'
import { ref } from "vue"
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 学员列表
const students = ref<Array<any>>([])
// 弹窗状态
const showAddModal = ref(false)
const showEditModal = ref(false)
// 表单数据
const formData = ref<any>({})
// 当前编辑的学员
const editingStudent = ref<any>({})
onLoad(() => {
getData()
})
onReachBottom(() => {
getList()
})
// 计算年龄
const calculateAge = (birthDate : string) : number => {
if (!birthDate) return 0
const birth = new Date(birthDate)
const today = new Date()
let age = today.getFullYear() - birth.getFullYear()
const monthDiff = today.getMonth() - birth.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--
}
return age
}
// 出生日期变化时自动计算年龄
const onBirthDateChange = (e : any) => {
formData.value.birthDate = e.detail.value
formData.value.age = calculateAge(e.detail.value).toString()
}
// 打开编辑弹窗
const openEditModal = (student : any) => {
editingStudent.value = student
formData.value = {
id: student.studentId,
name: student.name,
gender: student.sex ? student.sex : '男',
birthDate: student.birthday,
school: student.school,
address: student.address
}
showEditModal.value = true
}
// 关闭弹窗
const closeModal = () => {
showAddModal.value = false
showEditModal.value = false
editingStudent.value = null
formData.value = {
id: '',
name: '',
gender: '男',
age: '',
birthDate: '',
school: '',
address: ''
}
}
// 获取学员列表数据
const getData = () => {
students.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
// 获取学员列表
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
studentService.GetStudentListPage(page.value.toString()).then(res => {
if (res.code == 0) {
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 {
Service.Msg(res.msg)
}
})
}
// 保存学员
const saveStudent = () => {
if (!formData.value.name) {
Service.Msg('请输入学员姓名')
return
}
if (!formData.value.birthDate) {
Service.Msg('请选择出生日期')
return
}
// 构造接口请求数据
const requestData = {
studentId: formData.value.id ? formData.value.id : '',
name: formData.value.name,
sex: formData.value.gender,
birthday: formData.value.birthDate,
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(formData.value.id ? '修改成功!' : '添加成功!')
getData()
} else {
Service.Msg(content.msg)
}
})
closeModal()
}
// 确认删除
const confirmDelete = (student : any) => {
deleteStudent(student.studentId)
}
// 删除学员
const deleteStudent = (id : string) => {
studentService.Delete(id).then(res => {
if (res.code == 0) {
students.value = students.value.filter(s => s.id !== id)
Service.Msg('修改成功!')
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.student-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 180rpx;
}
/* 页面标题区域 */
.header-section {
margin-bottom: 24rpx;
.header-title {
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #999;
}
}
}
/* 学员列表区域 */
.students-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;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: #999;
}
}
}
.students-list {
.student-card {
background-color: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: flex-start;
justify-content: space-between;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.04);
}
.student-info {
display: flex;
align-items: flex-start;
gap: 20rpx;
flex: 1;
.student-avatar {
width: 72rpx;
height: 72rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.25);
flex-shrink: 0;
.avatar-text {
color: #fff;
font-size: 32rpx;
font-weight: 700;
}
}
.student-details {
flex: 1;
.detail-row {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.student-name {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
.student-gender {
display: flex;
align-items: center;
gap: 4rpx;
font-size: 24rpx;
font-weight: 500;
padding: 4rpx 12rpx;
border-radius: 12rpx;
&.male {
color: #1890ff;
background-color: #e6f7ff;
}
&.female {
color: #fa8c16;
background-color: #fff7e6;
}
}
.detail-label {
font-size: 24rpx;
color: #999;
flex-shrink: 0;
}
.detail-value {
font-size: 26rpx;
color: #666;
&.address-text {
font-size: 24rpx;
color: #888;
}
}
}
}
.student-actions {
display: flex;
gap: 12rpx;
flex-shrink: 0;
margin-top: 4rpx;
.action-btn {
width: 64rpx;
height: 64rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s ease;
&.edit-btn {
background-color: #e6f7ff;
&:active {
background-color: #bae7ff;
transform: scale(0.92);
}
}
&.delete-btn {
background-color: #f3f3f3;
&:active {
background-color: #f3f3f3;
transform: scale(0.92);
}
}
}
}
}
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 998;
animation: fadeIn 0.2s ease;
}
.student-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 640rpx;
background-color: #fff;
border-radius: 28rpx;
z-index: 999;
animation: slideUp 0.3s ease;
overflow: hidden;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.2);
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.modal-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
.modal-close {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s ease;
&:active {
background-color: #f5f5f5;
}
}
}
.modal-content {
padding: 36rpx 30rpx;
}
.modal-footer {
display: flex;
gap: 16rpx;
padding: 24rpx 30rpx 36rpx;
border-top: 1rpx solid #f0f0f0;
}
}
/* 表单样式 */
.form-group {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 14rpx;
}
.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);
}
&.date-picker {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
line-height: 88rpx;
&.placeholder {
color: #999;
}
}
}
.date-picker-wrapper {
position: relative;
picker {
width: 100%;
}
}
.age-display {
margin-top: 24rpx;
padding: 28rpx 32rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 100rpx;
height: 100rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
&::after {
content: '';
position: absolute;
bottom: -30%;
left: 10%;
width: 60rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.08);
border-radius: 50%;
}
.age-icon {
width: 72rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
backdrop-filter: blur(10rpx);
border: 2rpx solid rgba(255, 255, 255, 0.3);
}
.age-info {
flex: 1;
display: flex;
align-items: baseline;
gap: 8rpx;
.age-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.age-value {
font-size: 56rpx;
font-weight: 800;
color: #fff;
line-height: 1;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.age-unit {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
font-weight: 600;
}
}
}
.gender-selector {
display: flex;
gap: 20rpx;
.gender-option {
flex: 1;
height: 100rpx;
background-color: #f5f5f5;
border-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
font-size: 26rpx;
color: #666;
transition: all 0.3s ease;
border: 2rpx solid transparent;
position: relative;
overflow: hidden;
&:active {
transform: scale(0.96);
}
&.active {
font-weight: 600;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-color: #1890ff;
color: #fff;
box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.3);
.gender-icon-box {
background-color: rgba(255, 255, 255, 0.2);
}
}
.gender-icon-box {
width: 56rpx;
height: 56rpx;
background-color: #e6f7ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
}
}
}
/* 弹窗按钮 */
.modal-cancel-btn,
.modal-confirm-btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
border: none;
font-size: 30rpx;
font-weight: 600;
transition: all 0.25s ease;
}
.modal-cancel-btn {
background-color: #f5f5f5;
color: #666;
&:active {
background-color: #e8e8e8;
transform: scale(0.96);
}
}
.modal-confirm-btn {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
&:active {
transform: scale(0.96);
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.25);
}
}
/* 底部添加按钮 */
.bottom-add-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 20rpx;
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100;
.add-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
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);
}
.btn-text {
font-size: 32rpx;
color: #fff;
font-weight: 700;
}
}
}
/* 动画 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translate(-50%, -45%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
</style>

File diff suppressed because it is too large Load Diff

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.

BIN
src/static/index/Flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
src/static/index/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

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

@@ -0,0 +1,320 @@
## 2.5.0-202301012023-01-01
- 秋云图表组件 修改条件编译顺序确保uniapp的cli方式的项目依赖不完整时可以正常显示
- 秋云图表组件 恢复props属性directory的使用以修复vue3项目中开启echarts后echarts目录识别错误的bug
- uCharts.js 修复区域图、混合图只有一个数据时图表显示不正确的bug
- uCharts.js 修复折线图、区域图中时间轴类别图表tooltip指示点显示不正确的bug
- uCharts.js 修复x轴使用labelCount时并且boundaryGap = 'justify' 并且关闭Y轴显示的时候最后一个坐标值不显示的bug
- uCharts.js 修复折线图只有一组数据时 ios16 渲染颜色不正确的bug
- uCharts.js 修复玫瑰图半径显示不正确的bug
- uCharts.js 柱状图、山峰图增加正负图功能y轴网格如果需要显示0轴则由 min max 及 splitNumber 确定后续版本优化自动显示0轴
- uCharts.js 柱状图column增加 opts.extra.column.labelPosition数据标签位置有效值为 outside外部, insideTop内顶部, center内中间, bottom内底部
- uCharts.js 雷达图radar增加 opts.extra.radar.labelShow否显示各项标识文案是默认true
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.boxPadding提示窗边框填充距离默认3px
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.fontSize提示窗字体大小配置默认13px
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.lineHeight提示窗文字行高默认20px
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.legendShow是否显示左侧图例默认true
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.legendShape图例形状图例标识样式有效值为 auto自动跟随图例, diamond◆, circle●, triangle▲, square■, rect▬, line-
- uCharts.js 标记线markLine增加 opts.extra.markLine.labelFontSize字体大小配置默认13px
- uCharts.js 标记线markLine增加 opts.extra.markLine.labelPadding标签边框内填充距离默认6px
- uCharts.js 折线图line增加 opts.extra.line.linearType渐变色类型可选值 none关闭渐变色custom 自定义渐变色。使用自定义渐变色时请赋值serie.linearColor作为颜色值
- uCharts.js 折线图line增加 serie.linearColor渐变色数组格式为2维数组[起始位置,颜色值],例如[[0,'#0EE2F8'],[0.3,'#2BDCA8'],[0.6,'#1890FF'],[1,'#9A60B4']]
- uCharts.js 折线图line增加 opts.extra.line.onShadow是否开启折线阴影开启后请赋值serie.setShadow阴影设置
- uCharts.js 折线图line增加 serie.setShadow阴影配置格式为4位数组[offsetX,offsetY,blur,color]
- uCharts.js 折线图line增加 opts.extra.line.animation动画效果方向可选值为vertical 垂直动画效果horizontal 水平动画效果
- uCharts.js X轴xAxis增加 opts.xAxis.lineHeightX轴字体行高默认20px
- uCharts.js X轴xAxis增加 opts.xAxis.marginTopX轴文字距离轴线的距离默认0px
- uCharts.js X轴xAxis增加 opts.xAxis.title当前X轴标题
- uCharts.js X轴xAxis增加 opts.xAxis.titleFontSize标题字体大小默认13px
- uCharts.js X轴xAxis增加 opts.xAxis.titleOffsetY标题纵向偏移距离负数为向上偏移正数向下偏移
- uCharts.js X轴xAxis增加 opts.xAxis.titleOffsetX标题横向偏移距离负数为向左偏移正数向右偏移
- uCharts.js X轴xAxis增加 opts.xAxis.titleFontColor标题字体颜色默认#666666
## 报错TypeError: Cannot read properties of undefined (reading 'length')
- 如果是uni-modules版本组件请先登录HBuilderX账号
- 在HBuilderX中的manifest.json点击重新获取uniapp的appid或者删除appid重新粘贴重新运行
- 如果是cli项目请使用码云上的非uniCloud版本组件
- 或者添加uniCloud的依赖
- 或者使用原生uCharts
## 2.4.5-202211302022-11-30
- uCharts.js 优化tooltip当文字很多变为左侧显示时如果画布仍显显示不下提示框错位置变为以左侧0位置起画
- uCharts.js 折线图修复特殊情况下只有单点数据并改变线宽后点变为圆形的bug
- uCharts.js 修复Y轴disabled启用后无效并报错的bug
- uCharts.js 修复仪表盘起始结束角度特殊情况下显示不正确的bug
- uCharts.js 雷达图新增参数 opts.extra.radar.radius , 自定义雷达图半径
- uCharts.js 折线图、区域图增加tooltip指示点opts.extra.line.activeType/opts.extra.area.activeType可选值"none"不启用激活指示点,"hollow"空心点模式,"solid"实心点模式
## 2.4.4-202211022022-11-02
- 秋云图表组件 修复使用echarts时reload、reshow无法调用重新渲染的bug[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/40)
- 秋云图表组件 修复使用echarts时初始化时宽高不正确的bug[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/42)
- 秋云图表组件 修复uniapp的h5使用history模式时无法加载echarts的bug
- 秋云图表组件 小程序端@complete@scrollLeft@scrollRight@getTouchStart@getTouchMove@getTouchEnd事件增加opts参数传出,方便一些特殊需求的交互获取数据。
- uCharts.js 修复calTooltipYAxisData方法内formatter格式化方法未与y轴方法同步的问题[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/43)
- uCharts.js 地图新增参数opts.series[i].fillOpacity以透明度方式来设置颜色过度效果[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/38)
- uCharts.js 地图新增参数opts.extra.map.active是否启用点击激活变色
- uCharts.js 地图新增参数opts.extra.map.activeTextColor是否启用点击激活变色
- uCharts.js 地图新增渲染完成事件renderComplete
- uCharts.js 漏斗图修复当部分数据相同时tooltip提示窗点击错误的bug
- uCharts.js 漏斗图新增参数series.data[i].centerText 居中标签文案
- uCharts.js 漏斗图新增参数series.data[i].centerTextSize 居中标签文案字体大小默认opts.fontSize
- uCharts.js 漏斗图新增参数series.data[i].centerTextColor 居中标签文案字体颜色,默认#FFFFFF
- uCharts.js 漏斗图新增参数opts.extra.funnel.minSize 最小值的最小宽度默认0
- uCharts.js 进度条新增参数opts.extra.arcbar.direction动画方向可选值为cw顺时针、ccw逆时针
- uCharts.js 混合图新增参数opts.extra.mix.line.width折线的宽度默认2
- uCharts.js 修复tooltip开启horizentalLine水平横线标注时图表显示错位的bug
- uCharts.js 优化tooltip当文字很多变为左侧显示时如果画布仍显显示不下提示框错位置变为以左侧0位置起画
- uCharts.js 修复开启滚动条后X轴文字超出绘图区域后的隐藏逻辑
- uCharts.js 柱状图、条状图修复堆叠模式不能通过{value,color}赋值单个柱子颜色的问题
- uCharts.js 气泡图修复不识别series.textSize和series.textColor的bug
## 报错TypeError: Cannot read properties of undefined (reading 'length')
1. 如果是uni-modules版本组件请先登录HBuilderX账号
2. 在HBuilderX中的manifest.json点击重新获取uniapp的appid或者删除appid重新粘贴重新运行
3. 如果是cli项目请使用码云上的非uniCloud版本组件
4. 或者添加uniCloud的依赖
5. 或者使用原生uCharts
## 2.4.3-202205052022-05-05
- 秋云图表组件 修复开启canvas2d后将series赋值为空数组显示加载图标时再次赋值后画布闪动的bug
- 秋云图表组件 修复升级hbx最新版后ECharts的highlight方法报错的bug
- uCharts.js 雷达图新增参数opts.extra.radar.gridEval数据点位网格抽希默认1
- uCharts.js 雷达图新增参数opts.extra.radar.axisLabel 是否显示刻度点值默认false
- uCharts.js 雷达图新增参数opts.extra.radar.axisLabelTofix刻度点值小数位数默认0
- uCharts.js 雷达图新增参数opts.extra.radar.labelPointShow是否显示末端刻度圆点默认false
- uCharts.js 雷达图新增参数opts.extra.radar.labelPointRadius刻度圆点的半径默认3
- uCharts.js 雷达图新增参数opts.extra.radar.labelPointColor刻度圆点的颜色默认#cccccc
- uCharts.js 雷达图新增参数opts.extra.radar.linearType渐变色类型可选值"none"关闭渐变,"custom"开启渐变
- uCharts.js 雷达图新增参数opts.extra.radar.customColor自定义渐变颜色数组类型对应series的数组长度以匹配不同series颜色的不同配色方案例如["#FA7D8D", "#EB88E2"]
- uCharts.js 雷达图优化支持series.textColor、series.textSize属性
- uCharts.js 柱状图中温度计式图标优化支持全圆角类型修复边框有缝隙的bug详见官网【演示】中的温度计图表
- uCharts.js 柱状图新增参数opts.extra.column.activeWidth当前点击柱状图的背景宽度默认一个单元格单位
- uCharts.js 混合图增加opts.extra.mix.area.gradient 区域图是否开启渐变色
- uCharts.js 混合图增加opts.extra.mix.area.opacity 区域图透明度默认0.2
- uCharts.js 饼图、圆环图、玫瑰图、漏斗图增加opts.series[0].data[i].labelText自定义标签文字避免formatter格式化的繁琐详见官网【演示】中的饼图
- uCharts.js 饼图、圆环图、玫瑰图、漏斗图增加opts.series[0].data[i].labelShow自定义是否显示某一个指示标签避免因饼图类别太多导致标签重复或者居多导致图形变形的问题详见官网【演示】中的饼图
- uCharts.js 增加opts.series[i].legendText/opts.series[0].data[i].legendText与series.name同级自定义图例显示文字的方法
- uCharts.js 优化X轴、Y轴formatter格式化方法增加形参统一为fromatter:function(value,index,opts){}
- uCharts.js 修复横屏模式下无法使用双指缩放方法的bug
- uCharts.js 修复当只有一条数据或者多条数据值相等的时候Y轴自动计算的最大值错误的bug
- 【官网模板】增加外部自定义图例与图表交互的例子,[点击跳转](https://www.ucharts.cn/v2/#/layout/info?id=2)
## 注意非unimodules 版本如因更新 hbx 至 3.4.7 导致报错如下,请到码云更新非 unimodules 版本组件,[点击跳转](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6)
> Error in callback for immediate watcher "uchartsOpts": "SyntaxError: Unexpected token u in JSON at position 0"
## 2.4.2-202204212022-04-21
- 秋云图表组件 修复HBX升级3.4.6.20220420版本后echarts报错的问题
## 2.4.2-202204202022-04-20
## 重要此版本uCharts新增了很多功能修复了诸多已知问题
- 秋云图表组件 新增onzoom开启双指缩放功能仅uCharts前提需要直角坐标系类图表类型并且ontouch为true、opts.enableScroll为true详见实例项目K线图
- 秋云图表组件 新增optsWatch是否监听opts变化关闭optsWatch后动态修改opts不会触发图表重绘
- 秋云图表组件 修复开启canvas2d功能后动态更新数据后画布闪动的bug
- 秋云图表组件 去除directory属性改为自动获取echarts.min.js路径升级不受影响
- 秋云图表组件 增加getImage()方法及@getImage事件通过ref调用getImage()方法获,触发@getImage事件获取当前画布的base64图片文件流
- 秋云图表组件 支付宝、字节跳动、飞书、快手小程序支持开启canvas2d同层渲染设置。
- 秋云图表组件 新增加【非uniCloud】版本组件避免有些不需要uniCloud的使用组件发布至小程序需要提交隐私声明问题请到码云[【非uniCloud版本】](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6)或npm[【非uniCloud版本】](https://www.npmjs.com/package/@qiun/uni-ucharts)下载使用。
- uCharts.js 新增dobuleZoom双指缩放功能
- uCharts.js 新增山峰图type="mount"数据格式为饼图类格式不需要传入categories具体详见新版官网在线演示
- uCharts.js 修复折线图当数据中存在null时tooltip报错的bug
- uCharts.js 修复饼图类当画布比较小时自动计算的半径是负数报错的bug
- uCharts.js 统一各图表类型的series.formatter格式化方法的形参为(val, index, series, opts),方便格式化时有更多参数可用
- uCharts.js 标记线功能增加labelText自定义显示文字增加labelAlign标签显示位置左侧或右侧增加标签显示位置微调labelOffsetX、labelOffsetY
- uCharts.js 修复条状图当数值很小时开启圆角后样式错误的bug
- uCharts.js 修复X轴开启disabled后X轴仍占用空间的bug
- uCharts.js 修复X轴开启滚动条并且开启rotateLabel后X轴文字与滚动条重叠的bug
- uCharts.js 增加X轴rotateAngle文字旋转自定义角度取值范围(-90至90)
- uCharts.js 修复地图文字标签层级显示不正确的bug
- uCharts.js 修复饼图、圆环图、玫瑰图当数据全部为0的时候不显示数据标签的bug
- uCharts.js 修复当opts.padding上边距为0时Y轴顶部刻度标签位置不正确的bug
## 另外我们还开发了各大原生小程序组件已发布至码云和npm
[https://gitee.com/uCharts/uCharts](https://gitee.com/uCharts/uCharts)
[https://www.npmjs.com/~qiun](https://www.npmjs.com/~qiun)
## 对于原生uCharts文档我们已上线新版官方网站详情点击下面链接进入官网
[https://www.uCharts.cn/v2/](https://www.ucharts.cn/v2/)
## 2.3.7-202201222022-01-22
## 重要使用vue3编译请使用cli模式并升级至最新依赖HbuilderX编译需要使用3.3.8以上版本
- uCharts.js 修复uni-app平台组件模式使用vue3编译到小程序报错的bug。
## 2.3.7-202201182022-01-18
## 注意使用vue3的前提是需要3.3.8.20220114-alpha版本的HBuilder
## 2.3.67-202201182022-01-18
- 秋云图表组件 组件初步支持vue3全端编译会有些问题具体详见下面修改
1. 小程序端运行时在uni_modules文件夹的qiun-data-charts.js中搜索 new uni_modules_qiunDataCharts_js_sdk_uCharts_uCharts.uCharts将.uCharts去掉。
2. 小程序端发行时在uni_modules文件夹的qiun-data-charts.js中搜索 new e.uCharts将.uCharts去掉变为 new e。
3. 如果觉得上述步骤比较麻烦如果您的项目只编译到小程序端可以修改u-charts.js最后一行导出方式将 export default uCharts;变更为 export default { uCharts: uCharts }; 这样变更后H5和App端的renderjs会有问题请开发者自行选择。此问题非组件问题请等待DC官方修复Vue3的小程序端
## 2.3.6-202201112022-01-11
- 秋云图表组件 修改组件 props 属性中的 background 默认值为 rgba(0,0,0,0)
## 2.3.6-202112012021-12-01
- uCharts.js 修复bar条状图开启圆角模式时值很小时圆角渲染错误的bug
## 2.3.5-202110142021-10-15
- uCharts.js 增加vue3的编译支持仅原生uChartsqiun-data-charts组件后续会支持请关注更新
## 2.3.4-202110122021-10-12
- 秋云图表组件 修复 mac os x 系统 mouseover 事件丢失的 bug
## 2.3.3-202107062021-07-06
- uCharts.js 增加雷达图开启数据点值opts.dataLabel的显示
## 2.3.2-202106272021-06-27
- 秋云图表组件 修复tooltipCustom个别情况下传值不正确报错TypeError: Cannot read property 'name' of undefined的bug
## 2.3.1-202106162021-06-16
- uCharts.js 修复圆角柱状图使用4角圆角时当数值过大时不正确的bug
## 2.3.0-202106122021-06-12
- uCharts.js 【重要】uCharts增加nvue兼容可在nvue项目中使用gcanvas组件渲染uCharts[详见码云uCharts-demo-nvue](https://gitee.com/uCharts/uCharts)
- 秋云图表组件 增加tapLegend属性是否开启图例点击交互事件
- 秋云图表组件 getIndex事件中增加返回uCharts实例中的opts参数以便在页面中调用参数
- 示例项目 pages/other/other.vue增加app端自定义tooltip的方法详见showOptsTooltip方法
## 2.2.1-202106032021-06-03
- uCharts.js 修复饼图、圆环图、玫瑰图当起始角度不为0时tooltip位置不准确的bug
- uCharts.js 增加温度计式柱状图开启顶部半圆形的配置
## 2.2.0-202105292021-05-29
- uCharts.js 增加条状图type="bar"
- 示例项目 pages/ucharts/ucharts.vue增加条状图的demo
## 2.1.7-202105242021-05-24
- uCharts.js 修复大数据量模式下曲线图不平滑的bug
## 2.1.6-202105232021-05-23
- 秋云图表组件 修复小程序端开启滚动条更新数据后滚动条位置不符合预期的bug
## 2.1.5-20210517022021-05-17
- uCharts.js 修复自定义Y轴min和max值为0时不能正确显示的bug
## 2.1.5-202105172021-05-17
- uCharts.js 修复Y轴自定义min和max时未按指定的最大值最小值显示坐标轴刻度的bug
## 2.1.4-202105162021-05-16
- 秋云图表组件 优化onWindowResize防抖方法
- 秋云图表组件 修复APP端uCharts更新数据时清空series显示loading图标后再显示图表图表抖动的bug
- uCharts.js 修复开启canvas2d后x轴、y轴、series自定义字体大小未按比例缩放的bug
- 示例项目 修复format-e.vue拼写错误导致app端使用uCharts渲染图表
## 2.1.3-202105132021-05-13
- 秋云图表组件 修改uCharts变更chartData数据为updateData方法支持带滚动条的数据动态打点
- 秋云图表组件 增加onWindowResize防抖方法 fix by ど誓言,如尘般染指流年づ
- 秋云图表组件 H5或者APP变更chartData数据显示loading图表时原数据闪现的bug
- 秋云图表组件 props增加errorReload禁用错误点击重新加载的方法
- uCharts.js 增加tooltip显示categoryx轴对应点位标题的功能opts.extra.tooltip.showCategory默认为false
- uCharts.js 修复mix混合图只有柱状图时tooltip的分割线显示位置不正确的bug
- uCharts.js 修复开启滚动条图表在拖动中动态打点滚动条位置不正确的bug
- uCharts.js 修复饼图类数据格式为echarts数据格式series为空数组报错的bug
- 示例项目 修改uCharts.js更新到v2.1.2版本后,@getIndex方法获取索引值变更为e.currentIndex.index
- 示例项目 pages/updata/updata.vue增加滚动条拖动更新数据动态打点的demo
- 示例项目 pages/other/other.vue增加errorReload禁用错误点击重新加载的demo
## 2.1.2-202105092021-05-09
秋云图表组件 修复APP端初始化时就传入chartData或lacaldata不显示图表的bug
## 2.1.1-202105092021-05-09
- 秋云图表组件 变更ECharts的eopts配置在renderjs内执行支持在config-echarts.js配置文件内写function配置。
- 秋云图表组件 修复APP端报错Prop being mutated: "onmouse"错误的bug。
- 秋云图表组件 修复APP端报错Error: Not FoundPage[6][-1,27] at view.umd.min.js:1的bug。
## 2.1.0-202105072021-05-07
- 秋云图表组件 修复初始化时就有数据或者数据更新的时候loading加载动画闪动的bug
- uCharts.js 修复x轴format方法categories为字符串类型时返回NaN的bug
- uCharts.js 修复series.textColor、legend.fontColor未执行全局默认颜色的bug
## 2.1.0-202105062021-05-06
- 秋云图表组件 修复极个别情况下报错item.properties undefined的bug
- 秋云图表组件 修复极个别情况下关闭加载动画reshow不起作用无法显示图表的bug
- 示例项目 pages/ucharts/ucharts.vue 增加时间轴折线图type="tline"、时间轴区域图type="tarea"、散点图type="scatter"、气泡图demotype="bubble"、倒三角形漏斗图opts.extra.funnel.type="triangle"、金字塔形漏斗图opts.extra.funnel.type="pyramid"
- 示例项目 pages/format-u/format-u.vue 增加X轴format格式化示例
- uCharts.js 升级至v2.1.0版本
- uCharts.js 修复 玫瑰图面积模式点击tooltip位置不正确的bug
- uCharts.js 修复 玫瑰图点击图例只剩一个类别显示空白的bug
- uCharts.js 修复 饼图类图点击图例其他图表tooltip位置某些情况下不准的bug
- uCharts.js 修复 x轴为矢量轴时间轴情况下点击tooltip位置不正确的bug
- uCharts.js 修复 词云图获取点击索引偶尔不准的bug
- uCharts.js 增加 直角坐标系图表X轴format格式化方法原生uCharts.js用法请使用formatter
- uCharts.js 增加 漏斗图扩展配置倒三角形opts.extra.funnel.type="triangle"金字塔形opts.extra.funnel.type="pyramid"
- uCharts.js 增加 散点图opts.type="scatter"、气泡图opts.type="bubble"
- 后期计划 完善散点图、气泡图增加markPoints标记点增加横向条状图。
## 2.0.0-202105022021-05-02
- uCharts.js 修复词云图获取点击索引不正确的bug
## 2.0.0-202105012021-05-01
- 秋云图表组件 修复QQ小程序、百度小程序在关闭动画效果情况下v-for循环使用图表显示不正确的bug
## 2.0.0-202104262021-04-26
- 秋云图表组件 修复QQ小程序不支持canvas2d的bug
- 秋云图表组件 修复钉钉小程序某些情况点击坐标计算错误的bug
- uCharts.js 增加 extra.column.categoryGap 参数柱状图类每个category点位X轴点柱子组之间的间距
- uCharts.js 增加 yAxis.data[i].titleOffsetY 参数,标题纵向偏移距离,负数为向上偏移,正数向下偏移
- uCharts.js 增加 yAxis.data[i].titleOffsetX 参数,标题横向偏移距离,负数为向左偏移,正数向右偏移
- uCharts.js 增加 extra.gauge.labelOffset 参数仪表盘标签文字径向便宜距离默认13px
## 2.0.0-20210422-22021-04-22
秋云图表组件 修复 formatterAssign 未判断 args[key] == null 的情况导致栈溢出的 bug
## 2.0.0-202104222021-04-22
- 秋云图表组件 修复H5、APP、支付宝小程序、微信小程序canvas2d模式下横屏模式的bug
## 2.0.0-202104212021-04-21
- uCharts.js 修复多行图例的情况下图例在上方或者下方时图例float为左侧或者右侧时第二行及以后的图例对齐方式不正确的bug
## 2.0.0-202104202021-04-20
- 秋云图表组件 修复微信小程序开启canvas2d模式后windows版微信小程序不支持canvas2d模式的bug
- 秋云图表组件 修改非uni_modules版本为v2.0版本qiun-data-charts组件
## 2.0.0-202104192021-04-19
## v1.0版本已停更建议转uni_modules版本组件方式调用点击右侧绿色【使用HBuilderX导入插件】即可使用示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
## 初次使用如果提示未注册&lt;qiun-data-charts&gt;组件请重启HBuilderX如仍不好用请重启电脑
## 如果是cli项目请尝试清理node_modules重新install还不行就删除项目再重新install。
## 此问题已于DCloud官方确认HBuilderX下个版本会修复。
## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn)
## <font color=#FF0000> 新手请先完整阅读帮助文档及常见问题3遍右侧蓝色按钮示例项目请看2遍 </font>
## [DEMO演示及在线生成工具v2.0文档https://demo.ucharts.cn](https://demo.ucharts.cn)
## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
- uCharts.js 修复混合图中柱状图单独设置颜色不生效的bug
- uCharts.js 修复多Y轴单独设置fontSize时开启canvas2d后未对应放大字体的bug
## 2.0.0-202104182021-04-18
- 秋云图表组件 增加directory配置修复H5端history模式下如果发布到二级目录无法正确加载echarts.min.js的bug
## 2.0.0-202104162021-04-16
## v1.0版本已停更建议转uni_modules版本组件方式调用点击右侧绿色【使用HBuilderX导入插件】即可使用示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
## 初次使用如果提示未注册&lt;qiun-data-charts&gt;组件请重启HBuilderX如仍不好用请重启电脑
## 如果是cli项目请尝试清理node_modules重新install还不行就删除项目再重新install。
## 此问题已于DCloud官方确认HBuilderX下个版本会修复。
## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn)
## <font color=#FF0000> 新手请先完整阅读帮助文档及常见问题3遍右侧蓝色按钮示例项目请看2遍 </font>
## [DEMO演示及在线生成工具v2.0文档https://demo.ucharts.cn](https://demo.ucharts.cn)
## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
- 秋云图表组件 修复APP端某些情况下报错`Not Found Page`的bugfix by 高级bug开发技术员
- 示例项目 修复APP端v-for循环某些情况下报错`Not Found Page`的bugfix by 高级bug开发技术员
- uCharts.js 修复非直角坐标系tooltip提示窗右侧超出未变换方向显示的bug
## 2.0.0-202104152021-04-15
- 秋云图表组件 修复H5端发布到二级目录下echarts无法加载的bug
- 秋云图表组件 修复某些情况下echarts.off('finished')移除监听事件报错的bug
## 2.0.0-202104142021-04-14
## v1.0版本已停更建议转uni_modules版本组件方式调用点击右侧绿色【使用HBuilderX导入插件】即可使用示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
## 初次使用如果提示未注册&lt;qiun-data-charts&gt;组件请重启HBuilderX如仍不好用请重启电脑
## 如果是cli项目请尝试清理node_modules重新install还不行就删除项目再重新install。
## 此问题已于DCloud官方确认HBuilderX下个版本会修复。
## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn)
## <font color=#FF0000> 新手请先完整阅读帮助文档及常见问题3遍右侧蓝色按钮示例项目请看2遍 </font>
## [DEMO演示及在线生成工具v2.0文档https://demo.ucharts.cn](https://demo.ucharts.cn)
## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
- 秋云图表组件 修复H5端在cli项目下ECharts引用地址错误的bug
- 示例项目 增加ECharts的formatter用法的示例(详见示例项目format-e.vue)
- uCharts.js 增加圆环图中心背景色的配置extra.ring.centerColor
- uCharts.js 修复微信小程序安卓端柱状图开启透明色后显示不正确的bug
## 2.0.0-202104132021-04-13
- 秋云图表组件 修复百度小程序多个图表真机未能正确获取根元素dom尺寸的bug
- 秋云图表组件 修复百度小程序横屏模式方向不正确的bug
- 秋云图表组件 修改ontouch时@getTouchStart@getTouchMove@getTouchEnd的触发条件
- uCharts.js 修复饼图类数据格式series属性不生效的bug
- uCharts.js 增加时序区域图 详见示例项目中ucharts.vue
## 2.0.0-20210412-22021-04-12
## v1.0版本已停更建议转uni_modules版本组件方式调用点击右侧绿色【使用HBuilderX导入插件】即可使用示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
## 初次使用如果提示未注册&lt;qiun-data-charts&gt;组件请重启HBuilderX。如仍不好用请重启电脑此问题已于DCloud官方确认HBuilderX下个版本会修复。
## [DEMO演示及在线生成工具v2.0文档https://demo.ucharts.cn](https://demo.ucharts.cn)
## [图表组件在uniCloudAdmin中的应用 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
- 秋云图表组件 修复uCharts在APP端横屏模式下不能正确渲染的bug
- 示例项目 增加ECharts柱状图渐变色、圆角柱状图、横向柱状图条状图的示例
## 2.0.0-202104122021-04-12
- 秋云图表组件 修复created中判断echarts导致APP端无法识别改回mounted中判断echarts初始化
- uCharts.js 修复2d模式下series.textOffset未乘像素比的bug
## 2.0.0-202104112021-04-11
## v1.0版本已停更建议转uni_modules版本组件方式调用点击右侧绿色【使用HBuilderX导入插件】即可使用示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
## 初次使用如果提示未注册<qiun-data-charts>组件请重启HBuilderX并清空小程序开发者工具缓存。
## [DEMO演示及在线生成工具v2.0文档https://demo.ucharts.cn](https://demo.ucharts.cn)
## [图表组件在uniCloudAdmin中的应用 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
- uCharts.js 折线图区域图增加connectNulls断点续连的功能详见示例项目中ucharts.vue
- 秋云图表组件 变更初始化方法为created变更type2d默认值为true优化2d模式下组件初始化后dom获取不到的bug
- 秋云图表组件 修复左右布局时右侧图表点击坐标错误的bug修复tooltip柱状图自定义颜色显示object的bug
## 2.0.0-202104102021-04-10
- 修复左右布局时右侧图表点击坐标错误的bug修复柱状图自定义颜色tooltip显示object的bug
- 增加标记线及柱状图自定义颜色的demo
## 2.0.0-202104092021-04-08
## v1.0版本已停更建议转uni_modules版本组件方式调用点击右侧【使用HBuilderX导入插件】即可体验DEMO演示及在线生成工具v2.0文档)[https://demo.ucharts.cn](https://demo.ucharts.cn)
## 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
- uCharts.js 修复钉钉小程序百度小程序measureText不准确的bug修复2d模式下饼图类activeRadius为按比例放大的bug
- 修复组件在支付宝小程序端点击位置不准确的bug
## 2.0.0-202104082021-04-07
- 修复组件在支付宝小程序端不能显示的bug目前支付宝小程不能点击交互后续修复
- uCharts.js 修复高分屏下柱状图类,圆弧进度条 自定义宽度不能按比例放大的bug
## 2.0.0-202104072021-04-06
## v1.0版本已停更建议转uni_modules版本组件方式调用点击右侧【使用HBuilderX导入插件】即可体验DEMO演示及在线生成工具v2.0文档)[https://demo.ucharts.cn](https://demo.ucharts.cn)
## 增加 通过tofix和unit快速格式化y轴的demo add by `howcode`
## 增加 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
## 2.0.0-202104062021-04-05
# 秋云图表组件+uCharts v2.0版本同步上线使用方法详见https://demo.ucharts.cn帮助页
## 2.0.02021-04-05
# 秋云图表组件+uCharts v2.0版本同步上线使用方法详见https://demo.ucharts.cn帮助页

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,162 @@
<template>
<view class="container loading1">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading1',
data() {
return {
};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading1 {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading1 .shape1 {
-webkit-animation: animation1shape1 0.5s ease 0s infinite alternate;
animation: animation1shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, 16px);
transform: translate(16px, 16px);
}
}
@keyframes animation1shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, 16px);
transform: translate(16px, 16px);
}
}
.loading1 .shape2 {
-webkit-animation: animation1shape2 0.5s ease 0s infinite alternate;
animation: animation1shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, 16px);
transform: translate(-16px, 16px);
}
}
@keyframes animation1shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, 16px);
transform: translate(-16px, 16px);
}
}
.loading1 .shape3 {
-webkit-animation: animation1shape3 0.5s ease 0s infinite alternate;
animation: animation1shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, -16px);
transform: translate(16px, -16px);
}
}
@keyframes animation1shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, -16px);
transform: translate(16px, -16px);
}
}
.loading1 .shape4 {
-webkit-animation: animation1shape4 0.5s ease 0s infinite alternate;
animation: animation1shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, -16px);
transform: translate(-16px, -16px);
}
}
@keyframes animation1shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, -16px);
transform: translate(-16px, -16px);
}
}
</style>

View File

@@ -0,0 +1,170 @@
<template>
<view class="container loading2">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading2',
data() {
return {
};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading2 {
-webkit-transform: rotate(10deg);
transform: rotate(10deg);
}
.container.loading2 .shape {
border-radius: 5px;
}
.container.loading2{
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading2 .shape1 {
-webkit-animation: animation2shape1 0.5s ease 0s infinite alternate;
animation: animation2shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
@keyframes animation2shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
.loading2 .shape2 {
-webkit-animation: animation2shape2 0.5s ease 0s infinite alternate;
animation: animation2shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
@keyframes animation2shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
.loading2 .shape3 {
-webkit-animation: animation2shape3 0.5s ease 0s infinite alternate;
animation: animation2shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
@keyframes animation2shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
.loading2 .shape4 {
-webkit-animation: animation2shape4 0.5s ease 0s infinite alternate;
animation: animation2shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
@keyframes animation2shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<view class="container loading3">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading3',
data() {
return {
};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading3 {
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading3 .shape1 {
border-top-left-radius: 10px;
}
.container.loading3 .shape2 {
border-top-right-radius: 10px;
}
.container.loading3 .shape3 {
border-bottom-left-radius: 10px;
}
.container.loading3 .shape4 {
border-bottom-right-radius: 10px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading3 .shape1 {
-webkit-animation: animation3shape1 0.5s ease 0s infinite alternate;
animation: animation3shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
@keyframes animation3shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
.loading3 .shape2 {
-webkit-animation: animation3shape2 0.5s ease 0s infinite alternate;
animation: animation3shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
@keyframes animation3shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
.loading3 .shape3 {
-webkit-animation: animation3shape3 0.5s ease 0s infinite alternate;
animation: animation3shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
@keyframes animation3shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
.loading3 .shape4 {
-webkit-animation: animation3shape4 0.5s ease 0s infinite alternate;
animation: animation3shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
@keyframes animation3shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
</style>

View File

@@ -0,0 +1,222 @@
<template>
<view class="container loading5">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading5',
data() {
return {
};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading5 .shape {
width: 15px;
height: 15px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading5 .shape1 {
animation: animation5shape1 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
-webkit-transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
@keyframes animation5shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
-webkit-transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
.loading5 .shape2 {
animation: animation5shape2 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
-webkit-transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
@keyframes animation5shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
-webkit-transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
.loading5 .shape3 {
animation: animation5shape3 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
-webkit-transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
@keyframes animation5shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
-webkit-transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
.loading5 .shape4 {
animation: animation5shape4 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
-webkit-transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
@keyframes animation5shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
-webkit-transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<view class="container loading6">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading6',
data() {
return {
};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading6 {
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading6 .shape {
width: 12px;
height: 12px;
border-radius: 2px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading6 .shape1 {
-webkit-animation: animation6shape1 2s linear 0s infinite normal;
animation: animation6shape1 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
-webkit-transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
@keyframes animation6shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
-webkit-transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
.loading6 .shape2 {
-webkit-animation: animation6shape2 2s linear 0s infinite normal;
animation: animation6shape2 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
-webkit-transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
@keyframes animation6shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
-webkit-transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
.loading6 .shape3 {
-webkit-animation: animation6shape3 2s linear 0s infinite normal;
animation: animation6shape3 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
-webkit-transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
@keyframes animation6shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
-webkit-transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
.loading6 .shape4 {
-webkit-animation: animation6shape4 2s linear 0s infinite normal;
animation: animation6shape4 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
-webkit-transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
@keyframes animation6shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
-webkit-transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<view>
<Loading1 v-if="loadingType==1"/>
<Loading2 v-if="loadingType==2"/>
<Loading3 v-if="loadingType==3"/>
<Loading4 v-if="loadingType==4"/>
<Loading5 v-if="loadingType==5"/>
</view>
</template>
<script>
import Loading1 from "./loading1.vue";
import Loading2 from "./loading2.vue";
import Loading3 from "./loading3.vue";
import Loading4 from "./loading4.vue";
import Loading5 from "./loading5.vue";
export default {
components:{Loading1,Loading2,Loading3,Loading4,Loading5},
name: 'qiun-loading',
props: {
loadingType: {
type: Number,
default: 2
},
},
data() {
return {
};
},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,422 @@
/*
* uCharts®
* 高性能跨平台图表库支持H5、APP、小程序微信/支付宝/百度/头条/QQ/360、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 通用配置项
// 主题颜色配置如每个图表类型需要不同主题请在对应图表类型上更改color属性
const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
const cfe = {
//demotype为自定义图表类型
"type": ["pie", "ring", "rose", "funnel", "line", "column", "area", "radar", "gauge","candle","demotype"],
//增加自定义图表类型如果需要categories请在这里加入您的图表类型例如最后的"demotype"
"categories": ["line", "column", "area", "radar", "gauge", "candle","demotype"],
//instance为实例变量承载属性option为eopts承载属性不要删除
"instance": {},
"option": {},
//下面是自定义format配置因除H5端外的其他端无法通过props传递函数只能通过此属性对应下标的方式来替换
"formatter":{
"tooltipDemo1":function(res){
let result = ''
for (let i in res) {
if (i == 0) {
result += res[i].axisValueLabel + '年销售额'
}
let value = '--'
if (res[i].data !== null) {
value = res[i].data
}
// #ifdef H5
result += '\n' + res[i].seriesName + '' + value + ' 万元'
// #endif
// #ifdef APP-PLUS
result += '<br/>' + res[i].marker + res[i].seriesName + '' + value + ' 万元'
// #endif
}
return result;
},
legendFormat:function(name){
return "自定义图例+"+name;
},
yAxisFormatDemo:function (value, index) {
return value + '元';
},
seriesFormatDemo:function(res){
return res.name + '年' + res.value + '元';
}
},
//这里演示了自定义您的图表类型的option可以随意命名之后在组件上 type="demotype" 后组件会调用这个花括号里的option如果组件上还存在eopts参数会将demotype与eopts中option合并后渲染图表。
"demotype":{
"color": color,
//在这里填写echarts的option即可
},
//下面是自定义配置,请添加项目所需的通用配置
"column": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'axis'
},
"grid": {
"top": 30,
"bottom": 50,
"right": 15,
"left": 40
},
"legend": {
"bottom": 'left',
},
"toolbox": {
"show": false,
},
"xAxis": {
"type": 'category',
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
"boundaryGap": true,
"data": []
},
"yAxis": {
"type": 'value',
"axisTick": {
"show": false,
},
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
},
"seriesTemplate": {
"name": '',
"type": 'bar',
"data": [],
"barwidth": 20,
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"line": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'axis'
},
"grid": {
"top": 30,
"bottom": 50,
"right": 15,
"left": 40
},
"legend": {
"bottom": 'left',
},
"toolbox": {
"show": false,
},
"xAxis": {
"type": 'category',
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
"boundaryGap": true,
"data": []
},
"yAxis": {
"type": 'value',
"axisTick": {
"show": false,
},
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
},
"seriesTemplate": {
"name": '',
"type": 'line',
"data": [],
"barwidth": 20,
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"area": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'axis'
},
"grid": {
"top": 30,
"bottom": 50,
"right": 15,
"left": 40
},
"legend": {
"bottom": 'left',
},
"toolbox": {
"show": false,
},
"xAxis": {
"type": 'category',
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
"boundaryGap": true,
"data": []
},
"yAxis": {
"type": 'value',
"axisTick": {
"show": false,
},
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
},
"seriesTemplate": {
"name": '',
"type": 'line',
"data": [],
"areaStyle": {},
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"pie": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item'
},
"grid": {
"top": 40,
"bottom": 30,
"right": 15,
"left": 15
},
"legend": {
"bottom": 'left',
},
"seriesTemplate": {
"name": '',
"type": 'pie',
"data": [],
"radius": '50%',
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"ring": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item'
},
"grid": {
"top": 40,
"bottom": 30,
"right": 15,
"left": 15
},
"legend": {
"bottom": 'left',
},
"seriesTemplate": {
"name": '',
"type": 'pie',
"data": [],
"radius": ['40%', '70%'],
"avoidLabelOverlap": false,
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
"labelLine": {
"show": true
},
},
},
"rose": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item'
},
"legend": {
"top": 'bottom'
},
"seriesTemplate": {
"name": '',
"type": 'pie',
"data": [],
"radius": "55%",
"center": ['50%', '50%'],
"roseType": 'area',
},
},
"funnel": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item',
"formatter": "{b} : {c}%"
},
"legend": {
"top": 'bottom'
},
"seriesTemplate": {
"name": '',
"type": 'funnel',
"left": '10%',
"top": 60,
"bottom": 60,
"width": '80%',
"min": 0,
"max": 100,
"minSize": '0%',
"maxSize": '100%',
"sort": 'descending',
"gap": 2,
"label": {
"show": true,
"position": 'inside'
},
"labelLine": {
"length": 10,
"lineStyle": {
"width": 1,
"type": 'solid'
}
},
"itemStyle": {
"bordercolor": '#fff',
"borderwidth": 1
},
"emphasis": {
"label": {
"fontSize": 20
}
},
"data": [],
},
},
"gauge": {
"color": color,
"tooltip": {
"formatter": '{a} <br/>{b} : {c}%'
},
"seriesTemplate": {
"name": '业务指标',
"type": 'gauge',
"detail": {"formatter": '{value}%'},
"data": [{"value": 50, "name": '完成率'}]
},
},
"candle": {
"xAxis": {
"data": []
},
"yAxis": {},
"color": color,
"title": {
"text": ''
},
"dataZoom": [{
"type": 'inside',
"xAxisIndex": [0, 1],
"start": 10,
"end": 100
},
{
"show": true,
"xAxisIndex": [0, 1],
"type": 'slider',
"bottom": 10,
"start": 10,
"end": 100
}
],
"seriesTemplate": {
"name": '',
"type": 'k',
"data": [],
},
}
}
export default cfe;

View File

@@ -0,0 +1,606 @@
/*
* uCharts®
* 高性能跨平台图表库支持H5、APP、小程序微信/支付宝/百度/头条/QQ/360、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 主题颜色配置如每个图表类型需要不同主题请在对应图表类型上更改color属性
const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
//事件转换函数主要用作格式化x轴为时间轴根据需求自行修改
const formatDateTime = (timeStamp, returnType)=>{
var date = new Date();
date.setTime(timeStamp * 1000);
var y = date.getFullYear();
var m = date.getMonth() + 1;
m = m < 10 ? ('0' + m) : m;
var d = date.getDate();
d = d < 10 ? ('0' + d) : d;
var h = date.getHours();
h = h < 10 ? ('0' + h) : h;
var minute = date.getMinutes();
var second = date.getSeconds();
minute = minute < 10 ? ('0' + minute) : minute;
second = second < 10 ? ('0' + second) : second;
if(returnType == 'full'){return y + '-' + m + '-' + d + ' '+ h +':' + minute + ':' + second;}
if(returnType == 'y-m-d'){return y + '-' + m + '-' + d;}
if(returnType == 'h:m'){return h +':' + minute;}
if(returnType == 'h:m:s'){return h +':' + minute +':' + second;}
return [y, m, d, h, minute, second];
}
const cfu = {
//demotype为自定义图表类型一般不需要自定义图表类型只需要改根节点上对应的类型即可
"type":["pie","ring","rose","word","funnel","map","arcbar","line","column","mount","bar","area","radar","gauge","candle","mix","tline","tarea","scatter","bubble","demotype"],
"range":["饼状图","圆环图","玫瑰图","词云图","漏斗图","地图","圆弧进度条","折线图","柱状图","山峰图","条状图","区域图","雷达图","仪表盘","K线图","混合图","时间轴折线","时间轴区域","散点图","气泡图","自定义类型"],
//增加自定义图表类型如果需要categories请在这里加入您的图表类型例如最后的"demotype"
//自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴矢量x轴类图表没有categories不需要加入categories
"categories":["line","column","mount","bar","area","radar","gauge","candle","mix","demotype"],
//instance为实例变量承载属性不要删除
"instance":{},
//option为opts及eopts承载属性不要删除
"option":{},
//下面是自定义format配置因除H5端外的其他端无法通过props传递函数只能通过此属性对应下标的方式来替换
"formatter":{
"yAxisDemo1":function(val, index, opts){return val+'元'},
"yAxisDemo2":function(val, index, opts){return val.toFixed(2)},
"xAxisDemo1":function(val, index, opts){return val+'年';},
"xAxisDemo2":function(val, index, opts){return formatDateTime(val,'h:m')},
"seriesDemo1":function(val, index, series, opts){return val+'元'},
"tooltipDemo1":function(item, category, index, opts){
if(index==0){
return '随便用'+item.data+'年'
}else{
return '其他我没改'+item.data+'天'
}
},
"pieDemo":function(val, index, series, opts){
if(index !== undefined){
return series[index].name+''+series[index].data+'元'
}
},
},
//这里演示了自定义您的图表类型的option可以随意命名之后在组件上 type="demotype" 后组件会调用这个花括号里的option如果组件上还存在opts参数会将demotype与opts中option合并后渲染图表。
"demotype":{
//我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置
"type": "line",
"color": color,
"padding": [15,10,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
},
"legend": {
},
"extra": {
"line": {
"type": "curve",
"width": 2
},
}
},
//下面是自定义配置,请添加项目所需的通用配置
"pie":{
"type": "pie",
"color": color,
"padding": [5,5,5,5],
"extra": {
"pie": {
"activeOpacity": 0.5,
"activeRadius": 10,
"offsetAngle": 0,
"labelWidth": 15,
"border": true,
"borderWidth": 3,
"borderColor": "#FFFFFF"
},
}
},
"ring":{
"type": "ring",
"color": color,
"padding": [5,5,5,5],
"rotate": false,
"dataLabel": true,
"legend": {
"show": true,
"position": "right",
"lineHeight": 25,
},
"title": {
"name": "收益率",
"fontSize": 15,
"color": "#666666"
},
"subtitle": {
"name": "70%",
"fontSize": 25,
"color": "#7cb5ec"
},
"extra": {
"ring": {
"ringWidth":30,
"activeOpacity": 0.5,
"activeRadius": 10,
"offsetAngle": 0,
"labelWidth": 15,
"border": true,
"borderWidth": 3,
"borderColor": "#FFFFFF"
},
},
},
"rose":{
"type": "rose",
"color": color,
"padding": [5,5,5,5],
"legend": {
"show": true,
"position": "left",
"lineHeight": 25,
},
"extra": {
"rose": {
"type": "area",
"minRadius": 50,
"activeOpacity": 0.5,
"activeRadius": 10,
"offsetAngle": 0,
"labelWidth": 15,
"border": false,
"borderWidth": 2,
"borderColor": "#FFFFFF"
},
}
},
"word":{
"type": "word",
"color": color,
"extra": {
"word": {
"type": "normal",
"autoColors": false
}
}
},
"funnel":{
"type": "funnel",
"color": color,
"padding": [15,15,0,15],
"extra": {
"funnel": {
"activeOpacity": 0.3,
"activeWidth": 10,
"border": true,
"borderWidth": 2,
"borderColor": "#FFFFFF",
"fillOpacity": 1,
"labelAlign": "right"
},
}
},
"map":{
"type": "map",
"color": color,
"padding": [0,0,0,0],
"dataLabel": true,
"extra": {
"map": {
"border": true,
"borderWidth": 1,
"borderColor": "#666666",
"fillOpacity": 0.6,
"activeBorderColor": "#F04864",
"activeFillColor": "#FACC14",
"activeFillOpacity": 1
},
}
},
"arcbar":{
"type": "arcbar",
"color": color,
"title": {
"name": "百分比",
"fontSize": 25,
"color": "#00FF00"
},
"subtitle": {
"name": "默认标题",
"fontSize": 15,
"color": "#666666"
},
"extra": {
"arcbar": {
"type": "default",
"width": 12,
"backgroundColor": "#E9E9E9",
"startAngle": 0.75,
"endAngle": 0.25,
"gap": 2
}
}
},
"line":{
"type": "line",
"color": color,
"padding": [15,10,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
},
"legend": {
},
"extra": {
"line": {
"type": "straight",
"width": 2,
"activeType": "hollow"
},
}
},
"tline":{
"type": "line",
"color": color,
"padding": [15,10,0,15],
"xAxis": {
"disableGrid": false,
"boundaryGap":"justify",
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
"data":[
{
"min":0,
"max":80
}
]
},
"legend": {
},
"extra": {
"line": {
"type": "curve",
"width": 2,
"activeType": "hollow"
},
}
},
"tarea":{
"type": "area",
"color": color,
"padding": [15,10,0,15],
"xAxis": {
"disableGrid": true,
"boundaryGap":"justify",
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
"data":[
{
"min":0,
"max":80
}
]
},
"legend": {
},
"extra": {
"area": {
"type": "curve",
"opacity": 0.2,
"addLine": true,
"width": 2,
"gradient": true,
"activeType": "hollow"
},
}
},
"column":{
"type": "column",
"color": color,
"padding": [15,15,0,5],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"data":[{"min":0}]
},
"legend": {
},
"extra": {
"column": {
"type": "group",
"width": 30,
"activeBgColor": "#000000",
"activeBgOpacity": 0.08
},
}
},
"mount":{
"type": "mount",
"color": color,
"padding": [15,15,0,5],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"data":[{"min":0}]
},
"legend": {
},
"extra": {
"mount": {
"type": "mount",
"widthRatio": 1.5,
},
}
},
"bar":{
"type": "bar",
"color": color,
"padding": [15,30,0,5],
"xAxis": {
"boundaryGap":"justify",
"disableGrid":false,
"min":0,
"axisLine":false
},
"yAxis": {
},
"legend": {
},
"extra": {
"bar": {
"type": "group",
"width": 30,
"meterBorde": 1,
"meterFillColor": "#FFFFFF",
"activeBgColor": "#000000",
"activeBgOpacity": 0.08
},
}
},
"area":{
"type": "area",
"color": color,
"padding": [15,15,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
},
"legend": {
},
"extra": {
"area": {
"type": "straight",
"opacity": 0.2,
"addLine": true,
"width": 2,
"gradient": false,
"activeType": "hollow"
},
}
},
"radar":{
"type": "radar",
"color": color,
"padding": [5,5,5,5],
"dataLabel": false,
"legend": {
"show": true,
"position": "right",
"lineHeight": 25,
},
"extra": {
"radar": {
"gridType": "radar",
"gridColor": "#CCCCCC",
"gridCount": 3,
"opacity": 0.2,
"max": 200,
"labelShow": true
},
}
},
"gauge":{
"type": "gauge",
"color": color,
"title": {
"name": "66Km/H",
"fontSize": 25,
"color": "#2fc25b",
"offsetY": 50
},
"subtitle": {
"name": "实时速度",
"fontSize": 15,
"color": "#1890ff",
"offsetY": -50
},
"extra": {
"gauge": {
"type": "default",
"width": 30,
"labelColor": "#666666",
"startAngle": 0.75,
"endAngle": 0.25,
"startNumber": 0,
"endNumber": 100,
"labelFormat": "",
"splitLine": {
"fixRadius": 0,
"splitNumber": 10,
"width": 30,
"color": "#FFFFFF",
"childNumber": 5,
"childWidth": 12
},
"pointer": {
"width": 24,
"color": "auto"
}
}
}
},
"candle":{
"type": "candle",
"color": color,
"padding": [15,15,0,15],
"enableScroll": true,
"enableMarkLine": true,
"dataLabel": false,
"xAxis": {
"labelCount": 4,
"itemCount": 40,
"disableGrid": true,
"gridColor": "#CCCCCC",
"gridType": "solid",
"dashLength": 4,
"scrollShow": true,
"scrollAlign": "left",
"scrollColor": "#A6A6A6",
"scrollBackgroundColor": "#EFEBEF"
},
"yAxis": {
},
"legend": {
},
"extra": {
"candle": {
"color": {
"upLine": "#f04864",
"upFill": "#f04864",
"downLine": "#2fc25b",
"downFill": "#2fc25b"
},
"average": {
"show": true,
"name": ["MA5","MA10","MA30"],
"day": [5,10,20],
"color": ["#1890ff","#2fc25b","#facc14"]
}
},
"markLine": {
"type": "dash",
"dashLength": 5,
"data": [
{
"value": 2150,
"lineColor": "#f04864",
"showLabel": true
},
{
"value": 2350,
"lineColor": "#f04864",
"showLabel": true
}
]
}
}
},
"mix":{
"type": "mix",
"color": color,
"padding": [15,15,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"disabled": false,
"disableGrid": false,
"splitNumber": 5,
"gridType": "dash",
"dashLength": 4,
"gridColor": "#CCCCCC",
"padding": 10,
"showTitle": true,
"data": []
},
"legend": {
},
"extra": {
"mix": {
"column": {
"width": 20
}
},
}
},
"scatter":{
"type": "scatter",
"color":color,
"padding":[15,15,0,15],
"dataLabel":false,
"xAxis": {
"disableGrid": false,
"gridType":"dash",
"splitNumber":5,
"boundaryGap":"justify",
"min":0
},
"yAxis": {
"disableGrid": false,
"gridType":"dash",
},
"legend": {
},
"extra": {
"scatter": {
},
}
},
"bubble":{
"type": "bubble",
"color":color,
"padding":[15,15,0,15],
"xAxis": {
"disableGrid": false,
"gridType":"dash",
"splitNumber":5,
"boundaryGap":"justify",
"min":0,
"max":250
},
"yAxis": {
"disableGrid": false,
"gridType":"dash",
"data":[{
"min":0,
"max":150
}]
},
"legend": {
},
"extra": {
"bubble": {
"border":2,
"opacity": 0.5,
},
}
}
}
export default cfu;

View File

@@ -0,0 +1,5 @@
# uCharts JSSDK说明
1、如不使用uCharts组件可直接引用u-charts.js打包编译后会`自动压缩`,压缩后体积约为`120kb`
2、如果120kb的体积仍需压缩请手到uCharts官网通过在线定制选择您需要的图表。
3、config-ucharts.js为uCharts组件的用户配置文件升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。
4、config-echarts.js为ECharts组件的用户配置文件升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,100 @@
{
"id": "qiun-data-charts",
"displayName": "秋云 ucharts echarts 高性能跨全端图表组件",
"version": "2.5.0-20230101",
"description": "uCharts 新增正负柱状图支持H5及APP用 ucharts echarts 渲染图表uniapp可视化首选组件",
"keywords": [
"ucharts",
"echarts",
"f2",
"图表",
"可视化"
],
"repository": "https://gitee.com/uCharts/uCharts",
"engines": {
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "474119"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/~qiun",
"type": "component-vue",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√"
},
"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,84 @@
![logo](https://img-blog.csdnimg.cn/4a276226973841468c1be356f8d9438b.png)
[![star](https://gitee.com/uCharts/uCharts/badge/star.svg?theme=gvp)](https://gitee.com/uCharts/uCharts/stargazers)
[![fork](https://gitee.com/uCharts/uCharts/badge/fork.svg?theme=gvp)](https://gitee.com/uCharts/uCharts/members)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![npm package](https://img.shields.io/npm/v/@qiun/ucharts.svg?style=flat-square)](https://www.npmjs.com/~qiun)
## uCharts简介
`uCharts`是一款基于`canvas API`开发的适用于所有前端应用的图表库,开发者编写一套代码,可运行到 Web、iOS、Android基于 uni-app / taro )、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等更多支持 canvas API 的平台。
## 官方网站
## [https://www.ucharts.cn](https://www.ucharts.cn)
## 快速体验
一套代码编到多个平台依次扫描二维码亲自体验uCharts图表跨平台效果其他平台请自行编译。
![](https://www.ucharts.cn/images/web/guide/qrcode20220224.png)
![](https://img-blog.csdnimg.cn/7d0115593ff24ac39a224fb7c6ed72a4.png)
## 致开发者
感谢各位开发者`五年`来对秋云及uCharts的支持uCharts的进步离不开各位开发者的鼓励与贡献。为更好的帮助各位开发者使用图表工具我们推出了新版官网增加了在线定制、问答社区、在线配置等一些增值服务为确保您能更好的应用图表组件建议您先`仔细阅读官网指南`以及`常见问题`,而不是下载下来`直接使用`。如仍然不能解决,请到`官网社区`或开通会员后加入`专属VIP会员群`提问将会很快得到回答。
## 视频教程
## [uCharts新手入门教程](https://www.bilibili.com/video/BV1qA411Q7se/?share_source=copy_web&vd_source=42a1242f9aaade6427736af69eb2e1d9)
## 社群支持
uCharts官方拥有5个2000人的QQ群及专属VIP会员群支持庞大的用户量证明我们一直在努力请各位放心使用uCharts的开源图表组件的开发团队付出了大量的时间与精力经过四来的考验不会有比较明显的bug请各位放心使用。如果您有更好的想法可以在`码云提交Pull Requests`以帮助更多开发者完成需求再次感谢各位对uCharts的鼓励与支持
#### 官方交流群
- 交流群1371774600已满
- 交流群2619841586已满
- 交流群3955340127已满
- 交流群4641669795已满
- 交流群5236294809只能扫码加入
![](https://www.ucharts.cn/images/web/qq5.jpg)
- 口令`uniapp`
#### 专属VIP会员群
- 开通会员后详见【账号详情】页面中顶部的滚动通知
- 口令`您的用户ID`
## 版权信息
uCharts始终坚持开源遵循 [Apache Licence 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 开源协议意味着您无需支付任何费用即可将uCharts应用到您的产品中。
注意这并不意味着您可以将uCharts应用到非法的领域比如涉及赌博暴力等方面。如因此产生纠纷或法律问题uCharts相关方及秋云科技不承担任何责任。
## 合作伙伴
[![DIY官网](https://www.ucharts.cn/images/web/guide/links/diy-gw.png)](https://www.diygw.com/)
[![HasChat](https://www.ucharts.cn/images/web/guide/links/haschat.png)](https://gitee.com/howcode/has-chat)
[![uViewUI](https://www.ucharts.cn/images/web/guide/links/uView.png)](https://www.uviewui.com/)
[![图鸟UI](https://www.ucharts.cn/images/web/guide/links/tuniao.png)](https://ext.dcloud.net.cn/plugin?id=7088)
[![thorui](https://www.ucharts.cn/images/web/guide/links/thorui.png)](https://ext.dcloud.net.cn/publisher?id=202)
[![FirstUI](https://www.ucharts.cn/images/web/guide/links/first.png)](https://www.firstui.cn/)
[![nProUI](https://www.ucharts.cn/images/web/guide/links/nPro.png)](https://ext.dcloud.net.cn/plugin?id=5169)
[![GraceUI](https://www.ucharts.cn/images/web/guide/links/grace.png)](https://www.graceui.com/)
## 更新记录
详见官网指南中说明,[点击此处查看](https://www.ucharts.cn/v2/#/guide/index?id=100)
## 相关链接
- [uCharts官网](https://www.ucharts.cn)
- [DCloud插件市场地址](https://ext.dcloud.net.cn/plugin?id=271)
- [uCharts码云开源托管地址](https://gitee.com/uCharts/uCharts) [![star](https://gitee.com/uCharts/uCharts/badge/star.svg?theme=gvp)](https://gitee.com/uCharts/uCharts/stargazers)
- [uCharts npm开源地址](https://www.ucharts.cn)
- [ECharts官网](https://echarts.apache.org/zh/index.html)
- [ECharts配置手册](https://echarts.apache.org/zh/option.html)
- [图表组件在项目中的应用 ReportPlus数据报表](https://www.ucharts.cn/v2/#/layout/info?id=1)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,101 @@
# Vue2 动态插槽名兼容性解决方案
## 问题描述
在Vue2环境中使用动态插槽名 `:name="dynamicSlotName"` 时,可能会遇到以下错误:
```
v-slot 不支持动态插槽名,请设置 scopedSlotsCompiler 为 augmented
```
## 解决方案
### 方案1配置Vue编译器推荐
在项目根目录创建或修改 `vue.config.js`
```javascript
module.exports = {
configureWebpack: {
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
},
// 设置scopedSlotsCompiler为augmented
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.scopedSlotsCompiler = 'augmented'
return options
})
}
}
```
### 方案2代码层面解决已实现
sl-table组件已经修改为兼容写法
```vue
<!-- 修改前有问题 -->
<slot :name="cell.slot" :row="tableData[cell.rowIndex]" :cell="cell">
<!-- 修改后兼容 -->
<template v-for="slotName in slotList" :key="slotName">
<slot
v-if="cell.slot === slotName"
:name="slotName"
:row="tableData[cell.rowIndex]"
:cell="cell">
</slot>
</template>
```
### 方案3uni-app项目配置
如果是uni-app项目`manifest.json` 中添加:
```json
{
"h5": {
"devServer": {
"disableHostCheck": true
}
},
"mp-weixin": {
"setting": {
"urlCheck": false
}
}
}
```
## 使用示例
### Vue2环境
```vue
<template>
<sl-table :columns="columns" :tableData="data">
<template slot="customSlot" slot-scope="{ row, cell }">
<view>{{ row.name }}</view>
</template>
</sl-table>
</template>
```
### 微信小程序环境
```vue
<template>
<sl-table columns="{{columns}}" tableData="{{data}}">
<template slot="customSlot" slot-scope="{{slotData}}">
<view>{{slotData.row.name}}</view>
</template>
</sl-table>
</template>
```
## 注意事项
1. 确保Vue版本为2.6+
2. 如果使用方案1需要重启开发服务器
3. 方案2已经内置在组件中无需额外配置
4. 微信小程序环境建议使用方案2的代码兼容方式

View File

@@ -0,0 +1,55 @@
## 1.5.72026-01-30
修复已知bug
## 1.5.62026-01-30
修复已知bug
## 1.5.52026-01-06
修复微信小程序兼容样式问题
## 1.5.42025-12-29
修复已知bug
## 1.5.32025-12-23
修复bug
## 1.5.22025-12-19
表头排序:新增表头排序功能
## 1.5.12025-12-10
优化
## 1.52025-12-10
1.单选/多选新增单选和多选功能支持受控模式selectedRows
2.序号列新增行号列功能showRowIndex支持自定义配置
## 1.4.12025-12-10
动态合并单元格:支持竖向动态合并单元格
## 1.42025-12-04
-**动态横向合并单元格**: 横向列合并单元格
## 1.3.62025-11-24
修复bug添加第一页的完成加载方法
## 1.3.52025-11-24
修复bug
## 1.3.42025-11-24
修改示例
## 1.3.32025-11-24
- ✅ empty插槽支持空数据插槽配置
- ✅ 更新columns表头样式配置项和tableData单元格样式配置项
## 1.3.22025-11-14
修复已知bug
## 1.3.12025-11-14
✅ 多级表头:支持到三级表头
## 1.3.02025-11-14
-**新增横向滚动支持**:表格支持横向滚动,自动处理宽度计算
-**新增固定列功能**:支持左侧和右侧固定列
-**固定列阴影效果**:滚动时固定列显示阴影提示
-**百分比宽度优化**百分比宽度自动转换为px确保精确显示
-**性能优化**:优化追加数据时的渲染性能,使用样式缓存机制
-**加载更多优化**:加载更多提示固定在可视区域,不随横向滚动
## 1.2.22025-11-13
固定高度场景下表头不参与滚动,新增上拉加载能力
## 1.2.12025-09-22
修复已知bug
## 1.2.02025-09-11
1.新增微信小程序适配支持
## 1.1.22025-08-08
更新readme.md文件
## 1.1.12025-08-08
修复bug
## 1.1.02025-08-08
适配vue3
## 1.0.12025-03-03
修改组件结构

View File

@@ -0,0 +1,853 @@
<template>
<view class="header-container">
<view :style="[getGridStyle]" class="header-grid">
<!-- 选择列 -->
<view
v-if="selection"
class="header-item selection-header"
:style="[getSelectionHeaderStyle]"
@click.stop="handleSelectAllClick"
>
<view
class="header-item-label selection-label"
@click.stop="handleSelectAllClick"
>
<checkbox
style="transform: scale(0.7)"
v-if="selection === 'multiple'"
:checked="isAllSelected"
:indeterminate="indeterminate"
color="#1890ff"
@click.stop="handleSelectAllClick"
/>
<text v-else></text>
</view>
</view>
<!-- 行号列 -->
<view
v-if="showRowIndex"
class="header-item row-index-header"
:style="[getRowIndexHeaderStyle]"
>
<view class="header-item-label">{{
(rowIndexConfig && rowIndexConfig.label) || "序号"
}}</view>
</view>
<!-- 数据列 -->
<view
v-for="(header, index) in gridCell"
:key="index"
:class="[
'header-item',
header.fixed === 'left' && scrollLeft_b > 0
? 'fixed-left-box-shadow'
: '',
header.fixed === 'right' && scrollLeft_b < allWidth - containerWidth
? 'fixed-right-box-shadow'
: '',
header.isLastColumn ? 'last-header-item' : '',
]"
:style="[cellStyles(index)]"
>
<view class="header-item-label">
<view class="label-text">{{ header.label }}</view>
<view
v-if="header.sort"
class="sort-wrapper"
:class="{
'sort-active': isCurrentSortColumn(header),
'sort-asc':
isCurrentSortColumn(header) &&
getCurrentSortOrder(header) === 'asc',
'sort-desc':
isCurrentSortColumn(header) &&
getCurrentSortOrder(header) === 'desc',
}"
@click.stop="handleSortChange(header)"
>
<view class="sort-icon">
<view
class="sort-icon-up"
:class="{
active:
isCurrentSortColumn(header) &&
getCurrentSortOrder(header) === 'asc',
}"
></view>
<view
class="sort-icon-down"
:class="{
active:
isCurrentSortColumn(header) &&
getCurrentSortOrder(header) === 'desc',
}"
></view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
/**
* 表头组件
* 支持最多三级表头
* @author shiliu
*
* @props {columns} 表头数据列表
*
* @tips 示例数据columns: [
{
label: '分类',
children: [],
prop: 'type',
width:'40%',
},
{
label: '总实收营业额(元)',
width:'30%',
children: [
{ label: '数值(占比)', prop: 'totalPercent', slot: 'customSlot',width:'100%', },
]
},
{
label: '总毛利额(元)',
width:'30%',
children: [
{ label: '数值(占比)', prop: 'name',width:'100%' },
]
},
],
*
*/
// Vue版本检测工具函数
const isVue2 = () => {
const Vue =
(typeof window !== "undefined" && window.Vue) ||
(typeof global !== "undefined" && global.Vue);
return Vue && Vue.version && Vue.version.startsWith("2.");
};
export default {
name: "DataTableHeader",
props: {
columns: {
type: Array,
default: () => [],
},
flattenColumns: {
type: Array,
default: () => [],
},
containerWidth: {
type: Number,
default: 0,
},
scrollLeft_b: {
type: Number,
default: 0,
},
allWidth: {
type: Number,
default: 0,
},
selection: {
type: [String, Boolean],
default: false,
},
isAllSelected: {
type: Boolean,
default: false,
},
indeterminate: {
type: Boolean,
default: false,
},
showRowIndex: {
type: Boolean,
default: false,
},
rowIndexConfig: {
type: Object,
default: () => ({
width: "60px",
label: "序号",
fixed: "left",
}),
},
debug: {
type: Boolean,
default: false,
},
defaultSort: {
type: Object,
default: () => {},
},
},
data() {
return {
timer: null,
// scrollLeft: 0
};
},
destroyed() {
this.timer && clearTimeout(this.timer);
this.timer = null;
},
computed: {
sortField() {
return this.defaultSort?.prop || "";
},
sortOrder() {
return this.defaultSort?.order || "";
},
getGridStyle() {
// 使用 flattenColumns包含选择列
const columnWidths = this.flattenColumns
.map((col) => {
const width = col.width || "1fr";
// 如果是百分比转换为px
if (
typeof width === "string" &&
width.endsWith("%") &&
this.containerWidth > 0
) {
const percent = parseFloat(width);
if (!isNaN(percent)) {
return `${(this.containerWidth * percent) / 100}px`;
}
}
return width;
})
.join(" ");
return {
display: "grid",
gridTemplateColumns: columnWidths,
width: "100%",
};
},
getSelectionHeaderStyle() {
// 获取最大层级,选择列需要跨所有层级
const getMaxLevel = (cols, currentLevel = 1) => {
let max = currentLevel;
cols.forEach((item) => {
if (item.children?.length) {
const childLevel = getMaxLevel(item.children, currentLevel + 1);
max = Math.max(max, childLevel);
}
});
return max;
};
const maxLevel = getMaxLevel(this.columns, 1);
return {
gridColumn: "1",
gridRow: `1 / span ${maxLevel}`,
position: "sticky",
left: "0",
zIndex: 1001,
backgroundColor: "#F5F6FA",
};
},
getRowIndexHeaderStyle() {
// 获取最大层级,行号列需要跨所有层级
const getMaxLevel = (cols, currentLevel = 1) => {
let max = currentLevel;
cols.forEach((item) => {
if (item.children?.length) {
const childLevel = getMaxLevel(item.children, currentLevel + 1);
max = Math.max(max, childLevel);
}
});
return max;
};
const maxLevel = getMaxLevel(this.columns, 1);
// 找到行号列在 flattenColumns 中的索引
const rowIndexColIndex = this.flattenColumns.findIndex(
(col) => col.isRowIndex
);
// 计算左侧偏移量
let leftOffset = "auto";
if (rowIndexColIndex >= 0 && this.rowIndexConfig?.fixed === "left") {
// 使用 getStickyOffset 方法计算正确的偏移量
leftOffset = this.getStickyOffset(rowIndexColIndex, "left", 1);
}
// 计算右侧偏移量
let rightOffset = "auto";
if (rowIndexColIndex >= 0 && this.rowIndexConfig?.fixed === "right") {
rightOffset = this.getStickyOffset(rowIndexColIndex, "right", 1);
}
// 确定 gridColumn 位置
let gridColumn = "1";
if (this.selection && rowIndexColIndex >= 0) {
// 如果启用了选择列,行号列应该在第二列
gridColumn = "2";
} else if (rowIndexColIndex >= 0) {
// 如果没有选择列,行号列在第一列
gridColumn = "1";
}
// zIndex 层级:选择列(1001) > 行号列(1002) > 数据列(1000)
// 确保行号列不会被数据列覆盖
const zIndexValue = this.rowIndexConfig?.fixed ? 1002 : 1;
return {
gridColumn: gridColumn,
gridRow: `1 / span ${maxLevel}`,
position: this.rowIndexConfig?.fixed ? "sticky" : "static",
left: leftOffset,
right: rightOffset,
zIndex: zIndexValue,
backgroundColor: "#F5F6FA",
};
},
gridCell() {
// 递归计算最大层级
const getMaxLevel = (cols, currentLevel = 1) => {
let max = currentLevel;
cols.forEach((item) => {
if (item.children?.length) {
const childLevel = getMaxLevel(item.children, currentLevel + 1);
max = Math.max(max, childLevel);
}
});
return max;
};
const maxLevel = getMaxLevel(this.columns, 1);
// 递归计算列的实际跨度(计算所有叶子节点的数量)
const getColspan = (item) => {
if (!item.children?.length) {
return 1;
}
return item.children.reduce((sum, child) => sum + getColspan(child), 0);
};
// 计算最底层的列数(所有叶子节点的数量)
const getTotalCols = (cols) => {
return cols.reduce((sum, item) => {
if (!item.children?.length) {
return sum + 1;
}
return sum + getTotalCols(item.children);
}, 0);
};
const totalCols = getTotalCols(this.columns);
// 初始化cells数组为每一层创建足够的单元格
let cells = Array(maxLevel)
.fill()
.map(() =>
Array(totalCols)
.fill(null)
.map(() => ({}))
);
// 填充实际的单元格数据
const fillCells = (columns, level, startIndex) => {
let currentIndex = startIndex;
columns.forEach((item) => {
const colspan = getColspan(item);
if (!item.children?.length) {
// 没有子列需要计算rowspan跨到最底层
const rowspan = maxLevel - level;
cells[level][currentIndex] = {
...item,
rowspan: rowspan,
colspan: 1,
display: true,
rowIndex: level,
colIndex: currentIndex,
};
// 填充下方被合并的行
for (let i = 1; i < rowspan; i++) {
if (level + i < maxLevel) {
cells[level + i][currentIndex] = {
...cells[level + i][currentIndex],
display: false,
rowIndex: level,
colIndex: currentIndex,
};
}
}
// 移动到下一个索引
currentIndex++;
} else {
// 有子列,需要合并列
cells[level][currentIndex] = {
...item,
rowspan: 1,
colspan: colspan,
display: true,
rowIndex: level,
colIndex: currentIndex,
};
// 填充右侧被合并的列
for (let i = 1; i < colspan; i++) {
cells[level][currentIndex + i] = {
...cells[level][currentIndex + i],
display: false,
rowIndex: level,
colIndex: currentIndex + i,
};
}
// 递归处理子列
item.children.forEach((child) => {
currentIndex = fillCells([child], level + 1, currentIndex);
});
}
});
return currentIndex;
};
fillCells(this.columns, 0, 0);
const flattened = cells.flat().filter((cell) => cell && cell.display);
flattened.forEach((cell) => {
if (!cell) return;
const colIndex = cell.colIndex ?? 0;
const span = cell.colspan ?? 1;
// 调整 colIndex 以匹配 flattenColumns 中的实际索引
// flattenColumns 的顺序:选择列(0) + 行号列(1) + 数据列(2+)
let adjustedColIndex = colIndex;
if (this.selection) {
adjustedColIndex += 1; // 加上选择列
}
if (this.showRowIndex) {
adjustedColIndex += 1; // 加上行号列
}
cell.colIndex = adjustedColIndex;
// 判断是否是最后一列
if (colIndex + span >= totalCols) {
cell.isLastColumn = true;
}
});
console.log(flattened);
return flattened;
},
flattenColumnList() {
const result = [];
const flatten = (cols, parent) => {
cols.forEach((col) => {
if (col.children && col.children.length) {
flatten(col.children, col);
} else {
if (parent?.width && col?.width) {
let pWidth = Number(parent.width.split("%")[0]) * 0.01;
let cWidth = Number(col.width.split("%")[0]) * 0.01;
result.push({
...col,
width: pWidth * cWidth * 100 + "%",
});
} else {
result.push(col);
}
// result.push(col)
}
});
};
flatten(this.columns, null);
return result;
},
cellStyles() {
return function (index) {
const styles = {};
this.gridCell.forEach((cell, index) => {
styles[index] = this.getCellStyle(cell, cell.colIndex);
});
return styles[index];
};
},
},
methods: {
parseWidth(widthStr) {
if (!widthStr) {
return {
percent: 0,
px: 0,
valid: false,
};
}
const normalized = String(widthStr).trim();
const numericValue = parseFloat(normalized);
if (Number.isNaN(numericValue)) {
return {
percent: 0,
px: 0,
valid: false,
};
}
if (normalized.endsWith("%")) {
return {
percent: numericValue,
px: 0,
valid: true,
};
}
if (normalized.endsWith("rpx")) {
if (typeof uni !== "undefined" && typeof uni.upx2px === "function") {
return {
percent: 0,
px: uni.upx2px(numericValue),
valid: true,
};
}
return {
percent: 0,
px: numericValue,
valid: true,
};
}
if (normalized.endsWith("px")) {
return {
percent: 0,
px: numericValue,
valid: true,
};
}
return {
percent: 0,
px: 0,
valid: false,
};
},
getStickyOffset(colIndex, direction = "left", colspan = 1) {
const columns = this.flattenColumns || [];
if (!columns.length) return "0px";
let percent = 0;
let px = 0;
if (direction === "left") {
for (let i = 0; i < colIndex; i++) {
const colWidth = columns[i]?.width;
if (!colWidth) continue;
// 如果是百分比需要转换为px使用容器宽度
if (
typeof colWidth === "string" &&
colWidth.endsWith("%") &&
this.containerWidth > 0
) {
const percentValue = parseFloat(colWidth);
if (!isNaN(percentValue)) {
px += (this.containerWidth * percentValue) / 100;
}
} else {
const { percent: p, px: x } = this.parseWidth(colWidth);
percent += p;
px += x;
}
}
} else {
for (let i = colIndex + colspan; i < columns.length; i++) {
const colWidth = columns[i]?.width;
if (!colWidth) continue;
// 如果是百分比需要转换为px使用容器宽度
if (
typeof colWidth === "string" &&
colWidth.endsWith("%") &&
this.containerWidth > 0
) {
const percentValue = parseFloat(colWidth);
if (!isNaN(percentValue)) {
px += (this.containerWidth * percentValue) / 100;
}
} else {
const { percent: p, px: x } = this.parseWidth(colWidth);
percent += p;
px += x;
}
}
}
if (percent && px) {
return `calc(${percent}% + ${px}px)`;
}
if (percent) {
return `${percent}%`;
}
return `${px}px`;
},
handleScroll(e) {
//添加防抖
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(() => {
let scrollLeft = e.detail.scrollLeft;
this.$emit("scroll", scrollLeft);
}, 100);
},
// 计算每个label的高度
getCellStyle(cell, colIndex) {
// colIndex 已经是调整后的索引(在 gridCell 中已经考虑了选择列和行号列)
// 直接使用 colIndex 作为 flattenColumns 中的索引
const actualColIndex = colIndex;
const columnMeta = this.flattenColumns[actualColIndex] || {};
const baseWidth = columnMeta.width;
let width = baseWidth;
// 处理列合并的宽度
if (cell.colspan > 1) {
const colWidths = [];
for (let i = 0; i < cell.colspan; i++) {
const actualIndex = actualColIndex + i;
colWidths.push(this.flattenColumns[actualIndex]?.width || "100px");
}
width =
colWidths.reduce((sum, w) => {
const numWidth = parseInt(w);
return sum + (isNaN(numWidth) ? 100 : numWidth);
}, 0) + "px";
}
// 判断是否是 fixed='right' 的列
const isFixedRight =
cell.fixed === "right" || columnMeta.fixed === "right";
// 判断是否是挨着 fixed='right' 列最近的左侧列
const isAdjacentToFixedRight =
this.isAdjacentToFixedRight(actualColIndex);
this.debug && console.log(cell);
return {
// width: width,
flex: baseWidth ? "none" : "1",
display: cell.display ? "flex" : "none",
color: cell.textColor || "#333",
fontWeight: cell.bold ? "bold" : "normal",
gridRow: `span ${cell.rowspan}`,
gridColumn: `span ${cell.colspan}`,
position: cell.fixed ? "sticky" : "static",
left:
cell.fixed === "left"
? this.getStickyOffset(actualColIndex, "left", cell.colspan)
: "auto",
right:
cell.fixed === "right"
? this.getStickyOffset(actualColIndex, "right", cell.colspan)
: "auto",
zIndex: cell.fixed ? 1000 : 1,
backgroundColor: "#F5F6FA",
borderLeft: isFixedRight ? "1px solid #E7EAF2" : undefined,
borderRight: isAdjacentToFixedRight ? "none" : undefined,
...(cell.headerStyle || {}),
};
},
// 判断当前列是否是挨着 fixed='right' 列最近的左侧列
isAdjacentToFixedRight(colIndex) {
const nextColIndex = colIndex + 1;
if (nextColIndex >= this.flattenColumns.length) {
return false;
}
const currentCol = this.flattenColumns[colIndex] || {};
const nextCol = this.flattenColumns[nextColIndex] || {};
// 当前列不是 fixed且下一列是 fixed='right'
return !currentCol.fixed && nextCol.fixed === "right";
},
// 处理全选点击
handleSelectAllClick() {
if (this.selection === "multiple") {
this.$emit("select-all", !this.isAllSelected);
}
},
/**
* 判断当前列是否是排序列
* @param {Object} header 表头配置
* @returns {Boolean}
*/
isCurrentSortColumn(header) {
return this.sortField === header.prop && this.sortOrder;
},
/**
* 获取当前列的排序顺序
* @param {Object} header 表头配置
* @returns {String} 'asc' | 'desc' | ''
*/
getCurrentSortOrder(header) {
if (this.isCurrentSortColumn(header)) {
return this.sortOrder;
}
return "";
},
/**
* 处理排序变化
* @param {Object} header 表头配置
*/
handleSortChange(header) {
if (!header.sort || !header.prop) {
return;
}
let newOrder = "desc"; // 默认降序
// 如果当前列已经是排序列
if (this.isCurrentSortColumn(header)) {
// 切换排序顺序desc -> asc -> desc
if (this.sortOrder === "desc") {
newOrder = "asc";
} else if (this.sortOrder === "asc") {
newOrder = "desc";
}
}
// 触发排序变化事件
this.$emit("sort-change", {
prop: header.prop,
order: newOrder,
});
},
},
};
</script>
<style lang="scss">
.header-container {
display: flex;
// width: max-content;
background-color: #f5f6fa;
// border-top: 1px solid #E7EAF2;
// border-left: 1px solid #E7EAF2;
box-sizing: border-box;
position: sticky;
top: 0;
left: 0;
z-index: 3;
.header-grid {
display: grid;
}
.header-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
border-right: 1px solid #e7eaf2;
border-bottom: 1px solid #e7eaf2;
box-sizing: border-box;
position: relative;
.header-item-label {
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f6fa;
padding: 8px;
font-size: 10px;
color: #1a1a1a;
text-align: center;
word-break: break-all;
box-sizing: border-box;
position: relative;
& + .header-item-label {
border-top: 1px solid #e7eaf2;
}
.label-text {
flex: 1;
text-align: center;
}
.sort-wrapper {
display: flex;
align-items: center;
justify-content: center;
margin-left: 4px;
cursor: pointer;
user-select: none;
padding: 2px;
transition: all 0.2s;
&:hover {
opacity: 0.8;
}
&.sort-active {
.sort-icon {
opacity: 1;
}
}
.sort-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 12px;
height: 14px;
opacity: 0.4;
transition: opacity 0.2s;
.sort-icon-up,
.sort-icon-down {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
transition: border-color 0.2s;
}
.sort-icon-up {
border-bottom: 5px solid #c0c4cc;
margin-bottom: 2px;
&.active {
border-bottom-color: #1890ff;
}
}
.sort-icon-down {
border-top: 5px solid #c0c4cc;
&.active {
border-top-color: #1890ff;
}
}
}
&.sort-asc .sort-icon,
&.sort-desc .sort-icon {
opacity: 1;
}
}
}
.flex {
display: flex;
border-top: 1px solid #e7eaf2; // 添加边框以确保对齐
}
.flex .header-item-label {
flex: 1;
& + .header-item-label {
border-left: 1px solid #e7eaf2;
}
}
&.last-header-item {
border-right: none;
}
&.selection-header {
cursor: pointer;
.selection-label {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More