矿机租赁系统代码更新
This commit is contained in:
3
power_leasing/.browserslistrc
Normal file
3
power_leasing/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
12
power_leasing/.env.development
Normal file
12
power_leasing/.env.development
Normal file
@@ -0,0 +1,12 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = m2pool
|
||||
|
||||
# 开发环境配置
|
||||
ENV = 'development'
|
||||
|
||||
#开发环境
|
||||
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
|
||||
# VUE_APP_BASE_API = 'http://18.183.240.108:8080/api/'
|
||||
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
|
||||
# 路由懒加载
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||
12
power_leasing/.env.production
Normal file
12
power_leasing/.env.production
Normal file
@@ -0,0 +1,12 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = m2pool
|
||||
|
||||
# 生产环境配置
|
||||
ENV = 'production'
|
||||
|
||||
# 生产环境
|
||||
VUE_APP_BASE_API = 'https://m2pool.com/api/'
|
||||
VUE_APP_BASE_URL = 'https://m2pool.com/'
|
||||
|
||||
# 路由懒加载
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||
15
power_leasing/.env.staging
Normal file
15
power_leasing/.env.staging
Normal file
@@ -0,0 +1,15 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = m2pool
|
||||
|
||||
NODE_ENV = production
|
||||
|
||||
# 测试环境配置
|
||||
ENV = 'staging'
|
||||
|
||||
# 测试环境
|
||||
# VUE_APP_BASE_API = 'http://18.183.240.108:8080/api/'
|
||||
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
|
||||
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
|
||||
|
||||
# 路由懒加载
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||
29
power_leasing/.eslintrc.js
Normal file
29
power_leasing/.eslintrc.js
Normal file
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'eslint:recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-redeclare': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-unused-vars': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-undef': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/no-unused-components': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-mixed-spaces-and-tabs': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-unreachable': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-const-assign': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/multi-word-component-names': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/no-parsing-error': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-empty': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
23
power_leasing/.gitignore
vendored
Normal file
23
power_leasing/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
292
power_leasing/README.md
Normal file
292
power_leasing/README.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Power Leasing - 电商系统
|
||||
|
||||
一个基于 Vue 2 + Element UI 的轻量级电商系统,包含商品展示、购物车、结算等完整功能。
|
||||
|
||||
## 🚀 功能特性
|
||||
|
||||
### 核心功能
|
||||
- **商品列表页面** - 展示所有商品,支持添加到购物车
|
||||
- **商品详情页面** - 商品详细信息展示,数量选择
|
||||
- **购物车页面** - 商品管理、数量修改、删除
|
||||
- **结算页面** - 订单摘要、收货信息填写、订单提交
|
||||
|
||||
### 技术特点
|
||||
- 轻量级架构,不依赖 Vuex 状态管理
|
||||
- 使用 localStorage 持久化购物车数据
|
||||
- 响应式设计,支持移动端
|
||||
- 完整的无障碍访问支持
|
||||
- 错误处理和用户反馈
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # 公共组件
|
||||
│ ├── header.vue # 顶部导航栏(含面包屑导航)
|
||||
│ └── content.vue # 内容容器
|
||||
├── Layout/ # 布局组件
|
||||
│ └── idnex.vue # 主布局
|
||||
├── views/ # 页面组件
|
||||
│ ├── productList/ # 商品列表
|
||||
│ │ └── index.vue
|
||||
│ ├── productDetail/ # 商品详情
|
||||
│ │ └── index.vue
|
||||
│ ├── cart/ # 购物车
|
||||
│ │ └── index.vue
|
||||
│ └── checkout/ # 结算页面
|
||||
│ └── index.vue
|
||||
├── utils/ # 工具函数
|
||||
│ ├── productService.js # 商品数据服务
|
||||
│ ├── cartManager.js # 购物车管理
|
||||
│ ├── navigation.js # 导航配置
|
||||
│ └── routeTest.js # 路由测试工具
|
||||
├── router/ # 路由配置
|
||||
│ ├── index.js # 主路由文件
|
||||
│ └── routes.js # 路由配置文件
|
||||
├── store/ # 状态管理(轻量使用)
|
||||
│ └── index.js
|
||||
├── App.vue # 根组件
|
||||
└── main.js # 入口文件
|
||||
```
|
||||
|
||||
## 🔧 技术栈
|
||||
|
||||
- **前端框架**: Vue 2.6.14
|
||||
- **UI 组件库**: Element UI 2.15.14
|
||||
- **路由**: Vue Router 3.5.1
|
||||
- **状态管理**: Vuex 3.6.2(轻量使用)
|
||||
- **样式**: SCSS + CSS Grid/Flexbox
|
||||
- **构建工具**: Vue CLI 5.0
|
||||
|
||||
## 🧭 路由配置
|
||||
|
||||
### 完整路由列表
|
||||
|
||||
| 路径 | 名称 | 描述 | 权限 |
|
||||
|------|------|------|------|
|
||||
| `/productList` | 商品列表 | 浏览所有可用商品 | all |
|
||||
| `/product/:id` | 商品详情 | 查看商品详细信息 | all |
|
||||
| `/cart` | 购物车 | 管理购物车商品 | all |
|
||||
| `/checkout` | 订单结算 | 完成订单结算 | all |
|
||||
|
||||
### 路由特性
|
||||
|
||||
- **嵌套路由**: 所有页面都在 Layout 组件内渲染
|
||||
- **动态路由**: 商品详情页支持动态 ID 参数
|
||||
- **路由守卫**: 自动设置页面标题和权限检查
|
||||
- **错误处理**: 404 页面自动重定向到商品列表
|
||||
- **面包屑导航**: 自动生成页面导航路径
|
||||
|
||||
### 路由文件结构
|
||||
|
||||
```
|
||||
src/router/
|
||||
├── index.js # 主路由文件,包含路由守卫和错误处理
|
||||
└── routes.js # 路由配置文件,按功能模块组织
|
||||
```
|
||||
|
||||
## 📱 页面说明
|
||||
|
||||
### 1. 商品列表页面 (`/productList`)
|
||||
- 网格布局展示所有商品
|
||||
- 商品卡片包含图片、标题、描述、价格
|
||||
- 支持点击查看详情
|
||||
- 一键添加到购物车
|
||||
|
||||
### 2. 商品详情页面 (`/product/:id`)
|
||||
- 商品图片和详细信息展示
|
||||
- **自定义数量选择器**:美观的加减按钮设计,支持1-99数量范围
|
||||
- 数量选择器特性:
|
||||
- 减号按钮(-):减少数量,最小值为1时自动禁用
|
||||
- 数量输入框:支持直接输入,实时验证范围
|
||||
- 加号按钮(+):增加数量,最大值为99时自动禁用
|
||||
- 悬停效果和焦点状态
|
||||
- 响应式设计,移动端适配
|
||||
- 添加到购物车功能
|
||||
- 返回商品列表
|
||||
|
||||
### 3. 购物车页面 (`/cart`)
|
||||
- 购物车商品列表
|
||||
- 数量修改和删除功能
|
||||
- 实时计算总价和商品数量
|
||||
- 清空购物车功能
|
||||
- 跳转到结算页面
|
||||
|
||||
### 4. 结算页面 (`/checkout`)
|
||||
- 订单摘要展示
|
||||
- 收货信息表单(姓名、电话、地址、备注)
|
||||
- 表单验证
|
||||
- 订单提交功能
|
||||
|
||||
## 🛠️ 安装和运行
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发环境运行
|
||||
npm run serve
|
||||
|
||||
# 生产环境构建
|
||||
npm run build
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 🧪 路由测试
|
||||
|
||||
项目包含完整的路由测试工具,可以在浏览器控制台中运行:
|
||||
|
||||
```javascript
|
||||
// 导入测试工具
|
||||
import { runFullTest } from './src/utils/routeTest'
|
||||
|
||||
// 运行完整测试
|
||||
runFullTest()
|
||||
|
||||
// 或者单独测试
|
||||
import { testRoutes, testNavigation } from './src/utils/routeTest'
|
||||
testRoutes()
|
||||
testNavigation()
|
||||
```
|
||||
|
||||
## 🔍 问题修复记录
|
||||
|
||||
### 页面重复渲染问题
|
||||
**问题描述**: 页面出现重复渲染,显示两次相同内容
|
||||
|
||||
**根本原因**:
|
||||
1. 路由配置存在冲突 - 两个相同路径的路由
|
||||
2. Layout 组件嵌套问题 - 多层 router-view 嵌套
|
||||
3. Vue 版本不匹配 - header.vue 使用了 Vue 3 语法
|
||||
|
||||
**解决方案**:
|
||||
1. 修复路由配置,移除重复路由
|
||||
2. 简化 Layout 组件结构,避免多层嵌套
|
||||
3. 将 header.vue 改为 Vue 2 语法
|
||||
4. 优化组件渲染逻辑
|
||||
|
||||
### 路由配置完善
|
||||
**新增功能**:
|
||||
1. 完整的路由配置文件 (`routes.js`)
|
||||
2. 导航配置工具 (`navigation.js`)
|
||||
3. 路由测试工具 (`routeTest.js`)
|
||||
4. 面包屑导航支持
|
||||
5. 路由守卫和错误处理
|
||||
|
||||
### 具体修复内容
|
||||
- `src/router/index.js`: 清理重复路由,添加电商页面路由
|
||||
- `src/router/routes.js`: 新增路由配置文件
|
||||
- `src/utils/navigation.js`: 新增导航配置工具
|
||||
- `src/utils/routeTest.js`: 新增路由测试工具
|
||||
- `src/components/header.vue`: Vue 3 → Vue 2 语法转换,添加面包屑导航
|
||||
- `src/components/content.vue`: 简化组件,移除不必要的 router-view
|
||||
- 新增完整的电商页面组件
|
||||
|
||||
## 🎨 设计原则
|
||||
|
||||
- **DRY 原则**: 避免代码重复,提取公共组件和工具函数
|
||||
- **KISS 原则**: 保持代码简单易懂
|
||||
- **SOLID 原则**: 单一职责,开闭原则
|
||||
- **YAGNI 原则**: 只实现当前需要的功能
|
||||
- **无障碍访问**: 支持键盘导航和屏幕阅读器
|
||||
|
||||
## 🔢 数量选择器组件
|
||||
|
||||
### 组件特性
|
||||
- **现代化设计**:圆角边框、阴影效果、悬停状态
|
||||
- **交互反馈**:按钮点击动画、焦点状态高亮
|
||||
- **数量验证**:自动限制范围(1-99),输入验证
|
||||
- **无障碍支持**:包含aria-label属性,支持键盘操作
|
||||
- **响应式布局**:移动端和桌面端自适应
|
||||
|
||||
### 使用方法
|
||||
```vue
|
||||
<template>
|
||||
<div class="quantity-selector">
|
||||
<button
|
||||
class="quantity-btn minus-btn"
|
||||
@click="handleDecreaseQuantity"
|
||||
:disabled="quantity <= 1"
|
||||
aria-label="减少数量"
|
||||
>
|
||||
<span class="btn-icon">−</span>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
class="quantity-input"
|
||||
v-model.number="quantity"
|
||||
:min="1"
|
||||
:max="99"
|
||||
aria-label="数量输入"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="quantity-btn plus-btn"
|
||||
@click="handleIncreaseQuantity"
|
||||
:disabled="quantity >= 99"
|
||||
aria-label="增加数量"
|
||||
>
|
||||
<span class="btn-icon">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 样式定制
|
||||
支持以下CSS类名和状态:
|
||||
- `.quantity-selector`:主容器样式
|
||||
- `.quantity-btn`:按钮基础样式
|
||||
- `.minus-btn` / `.plus-btn`:减号/加号按钮
|
||||
- `.quantity-input`:数量输入框
|
||||
- `.btn-icon`:按钮图标样式
|
||||
- 悬停状态、焦点状态、禁用状态
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
- 使用 CSS Grid 和 Flexbox 布局
|
||||
- 移动端优先的响应式设计
|
||||
- 支持触摸操作和手势
|
||||
- 适配不同屏幕尺寸
|
||||
|
||||
## 🔒 数据安全
|
||||
|
||||
- 购物车数据本地存储
|
||||
- 表单验证和错误处理
|
||||
- 用户输入过滤和清理
|
||||
- 安全的订单提交流程
|
||||
|
||||
## 🚀 未来优化
|
||||
|
||||
- [ ] 用户登录注册系统
|
||||
- [ ] 订单历史记录
|
||||
- [ ] 商品搜索和筛选
|
||||
- [ ] 支付集成
|
||||
- [ ] 商品评价系统
|
||||
- [ ] 库存管理
|
||||
- [ ] 后台管理系统
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.2.0 - 数量选择器组件优化 (2024)
|
||||
- ✨ 新增自定义数量选择器组件,替换Element UI数字输入框
|
||||
- 🎨 现代化UI设计:圆角边框、阴影效果、悬停状态
|
||||
- 🔧 功能增强:支持1-99数量范围,实时验证
|
||||
- ♿ 无障碍优化:添加aria-label属性,支持键盘操作
|
||||
- 📱 响应式设计:移动端和桌面端自适应
|
||||
- 🐛 修复:移除重复代码,优化组件结构
|
||||
- 📚 文档更新:添加组件使用说明和样式定制指南
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
---
|
||||
|
||||
**注意**: 这是一个演示项目,商品数据为静态数据,实际使用时需要连接后端 API。
|
||||
5
power_leasing/babel.config.js
Normal file
5
power_leasing/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
19
power_leasing/jsconfig.json
Normal file
19
power_leasing/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
8468
power_leasing/package-lock.json
generated
Normal file
8468
power_leasing/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
power_leasing/package.json
Normal file
33
power_leasing/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "power_leasing",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test": "vue-cli-service build --mode staging --dest test",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.11.0",
|
||||
"core-js": "^3.8.3",
|
||||
"element-ui": "^2.15.14",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^12.0.0",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
}
|
||||
}
|
||||
BIN
power_leasing/public/favicon.ico
Normal file
BIN
power_leasing/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
power_leasing/public/index.html
Normal file
17
power_leasing/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
35
power_leasing/src/App.vue
Normal file
35
power_leasing/src/App.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 6px;
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
power_leasing/src/Layout/idnex.vue
Normal file
28
power_leasing/src/Layout/idnex.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<el-container style="width: 100vw; height: 100vh" class="containerApp">
|
||||
<el-header class="el-header">
|
||||
<comHeard />
|
||||
</el-header>
|
||||
<el-main class="el-main">
|
||||
<appMain />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
comHeard: () => import("../components/header.vue"),
|
||||
appMain: () => import("../components/content.vue"),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
body,html,*{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
62
power_leasing/src/api/machine.js
Normal file
62
power_leasing/src/api/machine.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
//新增机器
|
||||
export function addSingleOrBatchMachine(data) {
|
||||
return request({
|
||||
url: `/lease/product/machine/addSingleOrBatchMachine`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//根据矿机id 删除商品矿机
|
||||
export function deleteMachine(data) {
|
||||
return request({
|
||||
url: `/lease/product/machine/delete`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//根据挖矿账户获取矿机列表
|
||||
export function getUserMachineList(data) {
|
||||
return request({
|
||||
url: `/lease/product/machine/getUserMachineList`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
//根据 登录账户 获取挖矿账户及挖矿币种集合
|
||||
export function getUserMinersList(data) {
|
||||
return request({
|
||||
url: `/lease/product/machine/getUserMinersList`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//编辑矿机 + 矿机上下架
|
||||
export function updateMachine(data) {
|
||||
return request({
|
||||
url: `/lease/product/machine/updateMachine`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
//获取矿机列表
|
||||
export function getMachineListForUpdate(data) {
|
||||
return request({
|
||||
url: `/lease/product/machine/getMachineListForUpdate`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
49
power_leasing/src/api/order.js
Normal file
49
power_leasing/src/api/order.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
//创建订单及订单详情
|
||||
export function addOrders(data) {
|
||||
return request({
|
||||
url: `/lease/order/info/addOrders`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//取消订单
|
||||
export function cancelOrder(data) {
|
||||
return request({
|
||||
url: `/lease/order/info/cancelOrder`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//根据订单id查询订单信息
|
||||
export function getOrdersByIds(data) {
|
||||
return request({
|
||||
url: `/lease/order/info/getOrdersByIds`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//查询订单列表(买家侧)
|
||||
export function getOrdersByStatus(data) {
|
||||
return request({
|
||||
url: `/lease/order/info/getOrdersByStatus`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//查询订单列表(卖家侧)
|
||||
export function getOrdersByStatusForSeller(data) {
|
||||
return request({
|
||||
url: `/lease/order/info/getOrdersByStatusForSeller`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
89
power_leasing/src/api/products.js
Normal file
89
power_leasing/src/api/products.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
//商品列表
|
||||
export function getList(data) {
|
||||
return request({
|
||||
url: `/lease/product/getList`,
|
||||
method: 'get',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//创建商品 新增商品
|
||||
export function createProduct(data) {
|
||||
return request({
|
||||
url: `/lease/product/add`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//获取商品列表
|
||||
export function getProductList(data) {
|
||||
return request({
|
||||
url: `/lease/product/getList`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新商品
|
||||
export function updateProduct(data) {
|
||||
return request({
|
||||
url: `/lease/product/update`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除商品
|
||||
export function deleteProduct(id) {
|
||||
return request({
|
||||
url: `/lease/product/delete`,
|
||||
method: 'post',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 查询单个商品详情
|
||||
export function getMachineInfo(data) {
|
||||
return request({
|
||||
url: `/lease/product/getMachineInfo`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 已购商品
|
||||
export function getOwnedList(data) {
|
||||
return request({
|
||||
url: `/lease/product/getOwnedList`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 已购商品详情
|
||||
export function getOwnedById(data) {
|
||||
return request({
|
||||
url: `/lease/product/getOwnedById`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 查商品详情里面的商品信息
|
||||
export function getMachineInfoById(data) {
|
||||
return request({
|
||||
url: `/lease/product/getMachineInfoById`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
35
power_leasing/src/api/shoppingCart.js
Normal file
35
power_leasing/src/api/shoppingCart.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
//加入购物车
|
||||
export function addCart(data) {
|
||||
return request({
|
||||
url: `/lease/shopping/cart/addGoods`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//查询购物车列表
|
||||
export function getGoodsList(data) {
|
||||
return request({
|
||||
url: `/lease/shopping/cart/getGoodsList`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
//删除购物车商品 批量
|
||||
export function deleteBatchGoods(data) {
|
||||
return request({
|
||||
url: `/lease/shopping/cart/deleteBatchGoods`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
94
power_leasing/src/api/shops.js
Normal file
94
power_leasing/src/api/shops.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
//商品列表
|
||||
export function getAddShop(data) {
|
||||
return request({
|
||||
url: `/lease/shop/addShop`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 我的店铺(获取当前用户店铺信息)
|
||||
export function getMyShop(params) {
|
||||
return request({
|
||||
url: `/lease/shop/getShopByUserEmail`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 更新店铺
|
||||
export function updateShop(data) {
|
||||
return request({
|
||||
url: `/lease/shop/updateShop`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除店铺
|
||||
export function deleteShop(id) {
|
||||
return request({
|
||||
url: `/lease/shop/deleteShop`,
|
||||
method: 'post',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询店铺信息(根据ID)
|
||||
export function queryShop(data) {
|
||||
return request({
|
||||
url: `/lease/shop/getShopById`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭店铺
|
||||
export function closeShop(id) {
|
||||
return request({
|
||||
url: `/lease/shop/closeShop`,
|
||||
method: 'post',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 根据 店铺id 查询店铺商品配置信息列表
|
||||
export function getShopConfig(id) {
|
||||
return request({
|
||||
url: `/lease/shop/getShopConfig`,
|
||||
method: 'post',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 新增商铺配置
|
||||
export function addShopConfig(data) {
|
||||
return request({
|
||||
url: `/lease/shop/addShopConfig`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 根据配置id 修改配置
|
||||
export function updateShopConfig(data) {
|
||||
return request({
|
||||
url: `/lease/shop/updateShopConfig`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 根据配置id 删除配置
|
||||
export function deleteShopConfig(data) {
|
||||
return request({
|
||||
url: `/lease/shop/deleteShopConfig`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
40
power_leasing/src/api/wallet.js
Normal file
40
power_leasing/src/api/wallet.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
//钱包余额
|
||||
export function getWalletInfo(data) {
|
||||
return request({
|
||||
url: `/lease/user/getWalletInfo`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//余额提现
|
||||
export function withdrawBalance(data) {
|
||||
return request({
|
||||
url: `/lease/user/withdrawBalance`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
//余额充值记录
|
||||
export function balanceRechargeList(data) {
|
||||
return request({
|
||||
url: `/lease/user/balanceRechargeList`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//提现记录
|
||||
export function balanceWithdrawList(data) {
|
||||
return request({
|
||||
url: `/lease/user/balanceWithdrawList`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
BIN
power_leasing/src/assets/logo.png
Normal file
BIN
power_leasing/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
60
power_leasing/src/components/HelloWorld.vue
Normal file
60
power_leasing/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
18
power_leasing/src/components/content.vue
Normal file
18
power_leasing/src/components/content.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="content-container">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Content'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-container {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
</style>
|
||||
267
power_leasing/src/components/header.vue
Normal file
267
power_leasing/src/components/header.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="navbar">
|
||||
<router-link
|
||||
v-for="nav in navigation"
|
||||
:key="nav.path"
|
||||
:to="nav.path"
|
||||
class="nav-btn"
|
||||
active-class="active"
|
||||
:title="nav.description"
|
||||
>
|
||||
<span class="nav-icon">{{ nav.icon }}</span>
|
||||
<span class="nav-text">{{ nav.name }}</span>
|
||||
<span v-if="nav.path === '/cart'" class="cart-count">({{ cartItemCount }})</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<!-- <div class="breadcrumb">
|
||||
<router-link
|
||||
v-for="(crumb, index) in breadcrumbs"
|
||||
:key="index"
|
||||
:to="getBreadcrumbPath(index)"
|
||||
class="breadcrumb-item"
|
||||
:class="{ active: index === breadcrumbs.length - 1 }"
|
||||
>
|
||||
{{ crumb }}
|
||||
</router-link>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { readCart } from '../utils/cartManager'
|
||||
import { mainNavigation, getBreadcrumb } from '../utils/navigation'
|
||||
import { getGoodsList } from '../api/shoppingCart'
|
||||
|
||||
export default {
|
||||
name: 'Header',
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
cart: [],
|
||||
// 服务端购物车数量(统计 productMachineDtoList 的总条数)
|
||||
cartServerCount: 0,
|
||||
navigation: mainNavigation
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cartItemCount() {
|
||||
// 只使用服务端数量,避免与本地缓存数量不一致
|
||||
return Number.isFinite(this.cartServerCount) ? this.cartServerCount : 0
|
||||
},
|
||||
breadcrumbs() {
|
||||
return getBreadcrumb(this.$route.path)
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
this.loadCart()
|
||||
// 监听购物车变化
|
||||
window.addEventListener('storage', this.handleStorageChange)
|
||||
// 首次加载服务端购物车数量
|
||||
this.loadServerCartCount()
|
||||
// 监听应用内购物车更新事件
|
||||
window.addEventListener('cart-updated', this.handleCartUpdated)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('storage', this.handleStorageChange)
|
||||
window.removeEventListener('cart-updated', this.handleCartUpdated)
|
||||
},
|
||||
methods: {
|
||||
loadCart() {
|
||||
this.cart = readCart()
|
||||
},
|
||||
async loadServerCartCount() {
|
||||
try {
|
||||
const res = await getGoodsList()
|
||||
// 统一提取 rows/数组
|
||||
const primary = Array.isArray(res && res.rows)
|
||||
? res.rows
|
||||
: Array.isArray(res && res.data && res.data.rows)
|
||||
? res.data.rows
|
||||
: Array.isArray(res && res.data)
|
||||
? res.data
|
||||
: (Array.isArray(res) ? res : [])
|
||||
|
||||
let groups = []
|
||||
if (Array.isArray(primary) && primary.length) {
|
||||
// 情况A:shop -> shoppingCartInfoDtoList -> productMachineDtoList
|
||||
if (Array.isArray(primary[0] && primary[0].shoppingCartInfoDtoList)) {
|
||||
primary.forEach(shop => {
|
||||
if (Array.isArray(shop && shop.shoppingCartInfoDtoList)) {
|
||||
groups.push(...shop.shoppingCartInfoDtoList)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 情况B:直接就是商品分组数组
|
||||
groups = primary
|
||||
}
|
||||
} else if (Array.isArray(res && res.shoppingCartInfoDtoList)) {
|
||||
// 情况C:返回对象直接有 shoppingCartInfoDtoList
|
||||
groups = res.shoppingCartInfoDtoList
|
||||
}
|
||||
|
||||
let total = 0
|
||||
if (groups.length) {
|
||||
total = groups.reduce((sum, g) => sum + (Array.isArray(g && g.productMachineDtoList) ? g.productMachineDtoList.length : 0), 0)
|
||||
} else if (Array.isArray(res && res.productMachineDtoList)) {
|
||||
// 情况D:根对象直接是机器列表
|
||||
total = res.productMachineDtoList.length
|
||||
}
|
||||
|
||||
this.cartServerCount = Number.isFinite(total) ? total : 0
|
||||
} catch (e) {
|
||||
// 忽略错误,保持当前显示
|
||||
}
|
||||
},
|
||||
handleStorageChange(event) {
|
||||
if (event.key === 'power_leasing_cart_v1') {
|
||||
this.loadCart()
|
||||
this.loadServerCartCount()
|
||||
}
|
||||
},
|
||||
handleCartUpdated(event) {
|
||||
// 支持事件携带数量 { detail: { count } }
|
||||
try {
|
||||
const next = event && event.detail && Number(event.detail.count)
|
||||
if (Number.isFinite(next)) {
|
||||
this.cartServerCount = next
|
||||
return
|
||||
}
|
||||
} catch (e) { /* ignore malformed event detail */ }
|
||||
// 无显式数量则主动刷新
|
||||
this.loadServerCartCount()
|
||||
},
|
||||
handleLogout() {
|
||||
this.user = null
|
||||
this.cart = []
|
||||
},
|
||||
getBreadcrumbPath(index) {
|
||||
const paths = ['/productList', '/cart', '/checkout']
|
||||
if (index === 0) return '/productList'
|
||||
if (index < paths.length) return paths[index - 1]
|
||||
return '/productList'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 16px 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
color: #2c3e50;
|
||||
cursor: pointer;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: #f8f9fa;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
background: #42b983;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cart-count {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 面包屑导航样式 */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin: 0 20px 20px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: #42b983;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.breadcrumb-item:not(:last-child)::after {
|
||||
content: '>';
|
||||
margin-left: 8px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin: 0 12px 16px 12px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
23
power_leasing/src/main.js
Normal file
23
power_leasing/src/main.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import ElementUI from 'element-ui';
|
||||
import 'element-ui/lib/theme-chalk/index.css';
|
||||
// 引入登录信息处理
|
||||
import './utils/loginInfo.js';
|
||||
// 全局输入防表情守卫(极简、无侵入)
|
||||
import { initNoEmojiGuard } from './utils/noEmojiGuard.js';
|
||||
|
||||
// console.log = ()=>{} //全局关闭打印
|
||||
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.use(ElementUI);
|
||||
// 初始化全局防表情拦截器
|
||||
initNoEmojiGuard();
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
38
power_leasing/src/router/index.js
Normal file
38
power_leasing/src/router/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import { mainRoutes } from './routes'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes: mainRoutes
|
||||
})
|
||||
|
||||
// 路由守卫 - 设置页面标题和权限检查
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 设置页面标题
|
||||
if (to.meta && to.meta.title) {
|
||||
document.title = `${to.meta.title} - Power Leasing`
|
||||
} else {
|
||||
document.title = 'Power Leasing - 电商系统'
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (to.meta && to.meta.allAuthority) {
|
||||
// 这里可以添加权限检查逻辑
|
||||
// 目前所有页面都是 ['all'] 权限,所以直接通过
|
||||
console.log(`访问页面: ${to.meta.title}, 权限: ${to.meta.allAuthority.join(', ')}`)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// 路由错误处理
|
||||
router.onError((error) => {
|
||||
console.error('路由错误:', error)
|
||||
// 可以在这里添加错误处理逻辑,比如跳转到错误页面
|
||||
})
|
||||
|
||||
export default router
|
||||
250
power_leasing/src/router/routes.js
Normal file
250
power_leasing/src/router/routes.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* @file 路由配置文件
|
||||
* @description 定义所有电商页面的路由配置
|
||||
*/
|
||||
|
||||
// 商品相关路由
|
||||
export const productRoutes = [
|
||||
{
|
||||
path: '/productList',
|
||||
name: 'productList',
|
||||
|
||||
component: () => import('../views/productList/index.vue'),
|
||||
meta: {
|
||||
title: '商品列表',
|
||||
description: '浏览所有可用商品',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/product/:id',
|
||||
name: 'productDetail',
|
||||
component: () => import('../views/productDetail/index.vue'),
|
||||
meta: {
|
||||
title: '商品详情',
|
||||
description: '查看商品详细信息',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 购物车相关路由
|
||||
export const cartRoutes = [
|
||||
{
|
||||
path: '/cart',
|
||||
name: 'cart',
|
||||
component: () => import('../views/cart/index.vue'),
|
||||
meta: {
|
||||
title: '购物车',
|
||||
description: '管理购物车商品',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 结算相关路由
|
||||
export const checkoutRoutes = [
|
||||
{
|
||||
path: '/checkout',
|
||||
name: 'checkout',
|
||||
component: () => import('../views/checkout/index.vue'),
|
||||
meta: {
|
||||
title: '订单结算',
|
||||
description: '完成订单结算',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 个人中心相关路由
|
||||
export const accountRoutes = [
|
||||
{
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
component: () => import('../views/account/index.vue'),
|
||||
redirect: '/account/wallet',
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
description: '管理个人资料和店铺',
|
||||
allAuthority: ['all']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'wallet',
|
||||
name: 'Wallet',
|
||||
component: () => import('../views/account/wallet.vue'),
|
||||
meta: {
|
||||
title: '我的钱包',
|
||||
description: '查看钱包余额、充值和提现',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{//充值记录
|
||||
path: 'rechargeRecord',
|
||||
name: 'RechargeRecord',
|
||||
component: () => import('../views/account/rechargeRecord.vue'),
|
||||
meta: {
|
||||
title: '充值记录',
|
||||
description: '查看充值记录',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{//提现记录
|
||||
path: 'withdrawalHistory',
|
||||
name: 'WithdrawalHistory',
|
||||
component: () => import('../views/account/withdrawalHistory.vue'),
|
||||
meta: {
|
||||
title: '提现记录',
|
||||
description: '查看提现记录',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'shop-new',
|
||||
name: 'accountShopNew',
|
||||
component: () => import('../views/account/shopNew.vue'),
|
||||
meta: {
|
||||
title: '新增店铺',
|
||||
description: '创建新的店铺',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'shop-config',
|
||||
name: 'accountShopConfig',
|
||||
component: () => import('../views/account/shopConfig.vue'),
|
||||
meta: {
|
||||
title: '店铺配置',
|
||||
description: '配置店铺收款和支付方式',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'shops',
|
||||
name: 'accountMyShops',
|
||||
component: () => import('../views/account/myShops.vue'),
|
||||
meta: {
|
||||
title: '我的店铺',
|
||||
description: '查看我的店铺信息',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'product-new',
|
||||
name: 'accountProductNew',
|
||||
component: () => import('../views/account/productNew.vue'),
|
||||
meta: {
|
||||
title: '新增商品',
|
||||
description: '创建新的商品',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
name: 'accountProducts',
|
||||
component: () => import('../views/account/products.vue'),
|
||||
meta: {
|
||||
title: '商品列表',
|
||||
description: '管理店铺下的商品列表',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'purchased',
|
||||
name: 'accountPurchased',
|
||||
component: () => import('../views/account/purchased.vue'),
|
||||
meta: {
|
||||
title: '已购商品',
|
||||
description: '查看已购买的商品列表',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'purchased-detail/:orderItemId',
|
||||
name: 'PurchasedDetail',
|
||||
component: () => import('../views/account/purchasedDetail.vue'),
|
||||
meta: {
|
||||
title: '已购商品详情',
|
||||
description: '查看已购商品详细信息',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'accountOrders',
|
||||
component: () => import('../views/account/orders.vue'),
|
||||
meta: {
|
||||
title: '订单列表',
|
||||
description: '查看与管理订单(按状态筛选)',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'seller-orders',
|
||||
name: 'accountSellerOrders',
|
||||
component: () => import('../views/account/SellerOrders.vue'),
|
||||
meta: {
|
||||
title: '已售出订单',
|
||||
description: '卖家侧订单列表',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'order-detail/:id',
|
||||
name: 'accountOrderDetail',
|
||||
component: () => import('../views/account/orderDetail.vue'),
|
||||
meta: {
|
||||
title: '订单详情',
|
||||
description: '查看订单详细信息',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'product-detail/:id',
|
||||
name: 'accountProductDetail',
|
||||
component: () => import('../views/account/productDetail.vue'),
|
||||
meta: {
|
||||
title: '商品详情',
|
||||
description: '个人中心 - 商品详情',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'product-machine-add',
|
||||
name: 'accountProductMachineAdd',
|
||||
component: () => import('../views/account/productMachineAdd.vue'),
|
||||
meta: {
|
||||
title: '添加出售机器',
|
||||
description: '为商品添加出售机器',
|
||||
allAuthority: ['all']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 所有子路由
|
||||
export const childrenRoutes = [
|
||||
...productRoutes,
|
||||
...cartRoutes,
|
||||
...checkoutRoutes,
|
||||
...accountRoutes
|
||||
]
|
||||
|
||||
// 主路由配置
|
||||
export const mainRoutes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../Layout/idnex.vue'),
|
||||
redirect: '/productList',
|
||||
children: childrenRoutes
|
||||
},
|
||||
// 404页面重定向到商品列表
|
||||
{
|
||||
path: '*',
|
||||
redirect: '/productList'
|
||||
}
|
||||
]
|
||||
|
||||
export default mainRoutes
|
||||
17
power_leasing/src/store/index.js
Normal file
17
power_leasing/src/store/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
128
power_leasing/src/utils/cartManager.js
Normal file
128
power_leasing/src/utils/cartManager.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @file 购物车管理(轻量,无 Vuex)
|
||||
* @description 提供添加、更新、删除、清空、查询购物车的函数。使用 localStorage 持久化。
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'power_leasing_cart_v1';
|
||||
|
||||
/**
|
||||
* @typedef {Object} CartItem
|
||||
* @property {string} id - 商品ID
|
||||
* @property {string} title - 商品标题
|
||||
* @property {number} price - 单价
|
||||
* @property {number} quantity - 数量
|
||||
* @property {string} image - 图片URL
|
||||
*/
|
||||
|
||||
/**
|
||||
* 读取本地购物车
|
||||
* @returns {CartItem[]}
|
||||
*/
|
||||
export const readCart = () => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter(Boolean);
|
||||
} catch (error) {
|
||||
console.error('[cartManager] readCart error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化购物车
|
||||
* @param {CartItem[]} cart
|
||||
*/
|
||||
const writeCart = (cart) => {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(cart));
|
||||
// 同步派发购物车更新事件(总数量),用于头部徽标等全局更新
|
||||
try {
|
||||
const count = cart.reduce((s, c) => s + Number(c.quantity || 0), 0)
|
||||
window.dispatchEvent(new CustomEvent('cart-updated', { detail: { count } }))
|
||||
} catch (e) { /* noop */ }
|
||||
} catch (error) {
|
||||
console.error('[cartManager] writeCart error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到购物车(若已存在则数量累加)
|
||||
* @param {CartItem} item
|
||||
* @returns {CartItem[]}
|
||||
*/
|
||||
export const addToCart = (item) => {
|
||||
if (!item || !item.id) return readCart();
|
||||
const cart = readCart();
|
||||
const index = cart.findIndex((c) => c.id === item.id);
|
||||
if (index >= 0) {
|
||||
const next = [...cart];
|
||||
next[index] = {
|
||||
...next[index],
|
||||
quantity: Math.max(1, Number(next[index].quantity || 0) + Number(item.quantity || 1))
|
||||
};
|
||||
writeCart(next);
|
||||
return next;
|
||||
}
|
||||
const next = [...cart, { ...item, quantity: Math.max(1, Number(item.quantity || 1)) }];
|
||||
writeCart(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数量
|
||||
* @param {string} productId
|
||||
* @param {number} quantity
|
||||
* @returns {CartItem[]}
|
||||
*/
|
||||
export const updateQuantity = (productId, quantity) => {
|
||||
const cart = readCart();
|
||||
const next = cart
|
||||
.map((c) => (c.id === productId ? { ...c, quantity: Math.max(1, Number(quantity) || 1) } : c));
|
||||
writeCart(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除商品
|
||||
* @param {string} productId
|
||||
* @returns {CartItem[]}
|
||||
*/
|
||||
export const removeFromCart = (productId) => {
|
||||
const cart = readCart();
|
||||
const next = cart.filter((c) => c.id !== productId);
|
||||
writeCart(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
* @returns {CartItem[]}
|
||||
*/
|
||||
export const clearCart = () => {
|
||||
writeCart([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算总价
|
||||
* @returns {{ totalQuantity: number, totalPrice: number }}
|
||||
*/
|
||||
export const computeSummary = () => {
|
||||
const cart = readCart();
|
||||
const totalQuantity = cart.reduce((sum, cur) => sum + Number(cur.quantity || 0), 0);
|
||||
const totalPrice = cart.reduce((sum, cur) => sum + Number(cur.quantity || 0) * Number(cur.price || 0), 0);
|
||||
return { totalQuantity, totalPrice };
|
||||
}
|
||||
|
||||
export default {
|
||||
readCart,
|
||||
addToCart,
|
||||
updateQuantity,
|
||||
removeFromCart,
|
||||
clearCart,
|
||||
computeSummary
|
||||
}
|
||||
|
||||
95
power_leasing/src/utils/coinList.js
Normal file
95
power_leasing/src/utils/coinList.js
Normal file
@@ -0,0 +1,95 @@
|
||||
export const coinList = [
|
||||
{
|
||||
path: "nexaAccess",
|
||||
value: "nexa",
|
||||
label: "nexa",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/nexa.png`,
|
||||
name: "course.NEXAcourse",
|
||||
show: true,
|
||||
amount: 10000,
|
||||
},
|
||||
{
|
||||
path: "grsAccess",
|
||||
value: "grs",
|
||||
label: "grs",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/grs.svg`,
|
||||
name: "course.GRScourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "monaAccess",
|
||||
value: "mona",
|
||||
label: "mona",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/mona.svg`,
|
||||
name: "course.MONAcourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "dgbsAccess",
|
||||
value: "dgbs",
|
||||
// label: "dgb-skein-pool1",
|
||||
label: "dgb(skein)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/dgb.svg`,
|
||||
name: "course.dgbsCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "dgbqAccess",
|
||||
value: "dgbq",
|
||||
// label: "dgb(qubit-pool1)",
|
||||
label: "dgb(qubit)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/dgb.svg`,
|
||||
name: "course.dgbqCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "dgboAccess",
|
||||
value: "dgbo",
|
||||
// label: "dgb-odocrypt-pool1",
|
||||
label: "dgb(odocrypt)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/dgb.svg`,
|
||||
name: "course.dgboCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "rxdAccess",
|
||||
value: "rxd",
|
||||
label: "radiant(rxd)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/rxd.png`,
|
||||
name: "course.RXDcourse",
|
||||
show: true,
|
||||
amount: 100,
|
||||
},
|
||||
{
|
||||
path: "enxAccess",
|
||||
value: "enx",
|
||||
label: "Entropyx(enx)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/enx.svg`,
|
||||
name: "course.ENXcourse",
|
||||
show: true,
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
path: "alphminingPool",
|
||||
value: "alph",
|
||||
label: "alephium",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/alph.svg`,
|
||||
name: "course.alphCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
]
|
||||
6
power_leasing/src/utils/errorCode.js
Normal file
6
power_leasing/src/utils/errorCode.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
'401': '认证失败,无法访问系统资源,请重新登录',
|
||||
'403': '当前操作没有权限',
|
||||
'404': '访问资源不存在',
|
||||
'default': '系统未知错误,请反馈给管理员'
|
||||
}
|
||||
74
power_leasing/src/utils/errorNotificationManager.js
Normal file
74
power_leasing/src/utils/errorNotificationManager.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 错误提示管理器
|
||||
* 用于控制错误提示的频率,避免短时间内重复显示相同类型的错误
|
||||
*/
|
||||
class ErrorNotificationManager {
|
||||
constructor() {
|
||||
// 记录最近显示的错误信息
|
||||
this.recentErrors = new Map();
|
||||
// 默认节流时间 (30秒)
|
||||
this.throttleTime = 3000;
|
||||
// 错误类型映射
|
||||
this.errorTypes = {
|
||||
'Network Error': 'network',
|
||||
'timeout': 'timeout',
|
||||
'Request failed with status code': 'statusCode',
|
||||
// 添加网络状态类型
|
||||
'networkReconnected': 'networkStatus',
|
||||
'NetworkError': 'network'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误类型
|
||||
* @param {String} message 错误信息
|
||||
* @returns {String} 错误类型
|
||||
*/
|
||||
getErrorType(message) {
|
||||
for (const [key, type] of Object.entries(this.errorTypes)) {
|
||||
if (message.includes(key)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以显示错误
|
||||
* @param {String} message 错误信息
|
||||
* @returns {Boolean} 是否可以显示
|
||||
*/
|
||||
canShowError(message) {
|
||||
const errorType = this.getErrorType(message);
|
||||
const now = Date.now();
|
||||
|
||||
// 检查同类型的错误是否最近已经显示过
|
||||
if (this.recentErrors.has(errorType)) {
|
||||
const lastTime = this.recentErrors.get(errorType);
|
||||
if (now - lastTime < this.throttleTime) {
|
||||
console.log(`[错误提示] 已抑制重复错误: ${errorType}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新最后显示时间
|
||||
this.recentErrors.set(errorType, now);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的错误记录
|
||||
*/
|
||||
cleanup() {
|
||||
const now = Date.now();
|
||||
this.recentErrors.forEach((time, type) => {
|
||||
if (now - time > this.throttleTime) {
|
||||
this.recentErrors.delete(type);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const errorNotificationManager = new ErrorNotificationManager();
|
||||
export default errorNotificationManager;
|
||||
68
power_leasing/src/utils/loadingManager.js
Normal file
68
power_leasing/src/utils/loadingManager.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// 全局 loading 状态管理器
|
||||
class LoadingManager {
|
||||
constructor() {
|
||||
this.loadingStates = new Map(); // 存储所有 loading 状态
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
// 监听网络重试完成事件
|
||||
window.addEventListener('network-retry-complete', () => {
|
||||
this.resetAllLoadingStates();
|
||||
});
|
||||
}
|
||||
|
||||
// 设置 loading 状态
|
||||
setLoading(componentId, stateKey, value) {
|
||||
const key = `${componentId}:${stateKey}`;
|
||||
this.loadingStates.set(key, {
|
||||
value,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// 获取 loading 状态
|
||||
getLoading(componentId, stateKey) {
|
||||
const key = `${componentId}:${stateKey}`;
|
||||
const state = this.loadingStates.get(key);
|
||||
return state ? state.value : false;
|
||||
}
|
||||
|
||||
// 重置所有 loading 状态
|
||||
resetAllLoadingStates() {
|
||||
// 清除所有处于加载状态的组件
|
||||
const componentsToUpdate = [];
|
||||
|
||||
this.loadingStates.forEach((state, key) => {
|
||||
if (state.value === true) {
|
||||
const [componentId, stateKey] = key.split(':');
|
||||
componentsToUpdate.push({ componentId, stateKey });
|
||||
this.loadingStates.set(key, { value: false, timestamp: Date.now() });
|
||||
}
|
||||
});
|
||||
|
||||
// 使用事件通知各组件更新
|
||||
window.dispatchEvent(new CustomEvent('reset-loading-states', {
|
||||
detail: { componentsToUpdate }
|
||||
}));
|
||||
}
|
||||
|
||||
// 重置特定组件的所有 loading 状态
|
||||
resetComponentLoadingStates(componentId) {
|
||||
const componentsToUpdate = [];
|
||||
|
||||
this.loadingStates.forEach((state, key) => {
|
||||
if (key.startsWith(`${componentId}:`) && state.value === true) {
|
||||
const stateKey = key.split(':')[1];
|
||||
componentsToUpdate.push({ componentId, stateKey });
|
||||
this.loadingStates.set(key, { value: false, timestamp: Date.now() });
|
||||
}
|
||||
});
|
||||
|
||||
return componentsToUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const loadingManager = new LoadingManager();
|
||||
export default loadingManager;
|
||||
113
power_leasing/src/utils/loginInfo.js
Normal file
113
power_leasing/src/utils/loginInfo.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 解密函数(与发送端保持一致)
|
||||
* @param {string} encryptedText - 加密的文本
|
||||
* @param {string} secretKey - 密钥
|
||||
* @returns {string} 解密后的字符串
|
||||
*/
|
||||
function decryptData(encryptedText, secretKey) {
|
||||
try {
|
||||
// Base64解码
|
||||
const encrypted = atob(encryptedText);
|
||||
let decrypted = '';
|
||||
for (let i = 0; i < encrypted.length; i++) {
|
||||
decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ secretKey.charCodeAt(i % secretKey.length));
|
||||
}
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
console.error('解密失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取并解密URL参数
|
||||
*/
|
||||
function getDecryptedParams() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const encryptedData = urlParams.get('data');
|
||||
const language = urlParams.get('language');
|
||||
const username = urlParams.get('username');
|
||||
const source = urlParams.get('source');
|
||||
const version = urlParams.get('version');
|
||||
|
||||
// 解密敏感数据
|
||||
const secretKey = 'mining-pool-secret-key-2024'; // 必须与发送端保持一致
|
||||
let sensitiveData = null;
|
||||
|
||||
if (encryptedData) {
|
||||
try {
|
||||
const decryptedJson = decryptData(encryptedData, secretKey);
|
||||
sensitiveData = JSON.parse(decryptedJson);
|
||||
} catch (error) {
|
||||
console.error('解密或解析数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 敏感数据(已解密)
|
||||
token: sensitiveData?.token || '',
|
||||
userEmail: sensitiveData?.userEmail || '',
|
||||
userId: sensitiveData?.userId || '',
|
||||
timestamp: sensitiveData?.timestamp || null,
|
||||
|
||||
// 非敏感数据(明文)
|
||||
language: language || 'zh',
|
||||
username: username || '',
|
||||
source: source || '',
|
||||
version: version || '1.0'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自动登录
|
||||
*/
|
||||
function performAutoLogin(token, userId, userEmail) {
|
||||
console.log('执行自动登录:', { userId, userEmail: userEmail ? '***' : '' });
|
||||
// 这里可以添加自动登录的逻辑
|
||||
// 例如:设置全局状态、跳转页面等
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置界面语言
|
||||
*/
|
||||
function setLanguage(language) {
|
||||
console.log('设置语言:', language);
|
||||
// 这里可以添加语言设置的逻辑
|
||||
// 例如:设置 i18n 语言、更新界面等
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const params = getDecryptedParams();
|
||||
if (params.token) {
|
||||
console.log(params.token,"params.token 存入");
|
||||
|
||||
localStorage.setItem('token', params.token);
|
||||
localStorage.setItem('userEmail', params.userEmail);
|
||||
localStorage.setItem('userId', params.userId);
|
||||
localStorage.setItem('language', params.language);
|
||||
localStorage.setItem('username', params.username);
|
||||
localStorage.setItem('source', params.source);
|
||||
localStorage.setItem('version', params.version);
|
||||
}
|
||||
|
||||
console.log('接收到的参数:', {
|
||||
userId: params.userId ? '***' : '',
|
||||
userEmail: params.userEmail ? '***' : '',
|
||||
token: params.token ? '***' : '',
|
||||
language: params.language,
|
||||
username: params.username,
|
||||
source: params.source
|
||||
});
|
||||
|
||||
// 根据参数执行相应操作
|
||||
if (params.token && params.userId) {
|
||||
// 执行自动登录
|
||||
performAutoLogin(params.token, params.userId, params.userEmail);
|
||||
}
|
||||
|
||||
if (params.language) {
|
||||
// 设置界面语言
|
||||
setLanguage(params.language);
|
||||
}
|
||||
});
|
||||
101
power_leasing/src/utils/navigation.js
Normal file
101
power_leasing/src/utils/navigation.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @file 导航配置文件
|
||||
* @description 定义所有可用的导航链接和菜单结构
|
||||
*/
|
||||
|
||||
// 主导航配置
|
||||
export const mainNavigation = [
|
||||
{
|
||||
path: '/productList',
|
||||
name: '商城',
|
||||
icon: '🛍️',
|
||||
description: '浏览所有商品'
|
||||
},
|
||||
{
|
||||
path: '/cart',
|
||||
name: '购物车',
|
||||
icon: '🛒',
|
||||
description: '管理购物车商品'
|
||||
},
|
||||
// {
|
||||
// path: '/checkout',
|
||||
// name: '结算',
|
||||
// icon: '💳',
|
||||
// description: '完成订单结算'
|
||||
// },
|
||||
{
|
||||
path: '/account',
|
||||
name: '个人中心',
|
||||
icon: '👤',
|
||||
description: '管理个人资料和店铺'
|
||||
}
|
||||
]
|
||||
|
||||
// 面包屑导航配置
|
||||
export const breadcrumbConfig = {
|
||||
'/productList': ['首页', '商品列表'],
|
||||
'/product': ['首页', '商品列表', '商品详情'],
|
||||
'/cart': ['首页', '购物车'],
|
||||
'/checkout': ['首页', '购物车', '订单结算'],
|
||||
'/account': ['首页', '个人中心'],
|
||||
'/account/wallet': ['首页', '个人中心', '我的钱包'],
|
||||
'/account/shop-new': ['首页', '个人中心', '新增店铺'],
|
||||
'/account/shop-config': ['首页', '个人中心', '店铺配置'],
|
||||
'/account/shops': ['首页', '个人中心', '我的店铺'],
|
||||
'/account/product-new': ['首页', '个人中心', '新增商品'],
|
||||
'/account/products': ['首页', '个人中心', '商品列表']
|
||||
}
|
||||
|
||||
// 获取面包屑导航
|
||||
export const getBreadcrumb = (path) => {
|
||||
// 处理动态路由
|
||||
if (path.startsWith('/product/')) {
|
||||
return breadcrumbConfig['/product']
|
||||
}
|
||||
|
||||
return breadcrumbConfig[path] || ['首页']
|
||||
}
|
||||
|
||||
// 检查路由权限
|
||||
export const checkRoutePermission = (route, userPermissions = []) => {
|
||||
if (!route.meta || !route.meta.allAuthority) {
|
||||
return true
|
||||
}
|
||||
|
||||
const requiredPermissions = route.meta.allAuthority
|
||||
|
||||
// 如果权限要求是 'all',则所有人都可以访问
|
||||
if (requiredPermissions.includes('all')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查用户是否有所需权限
|
||||
return requiredPermissions.some(permission =>
|
||||
userPermissions.includes(permission)
|
||||
)
|
||||
}
|
||||
|
||||
// 获取页面标题
|
||||
export const getPageTitle = (route) => {
|
||||
if (route.meta && route.meta.title) {
|
||||
return `${route.meta.title} - Power Leasing`
|
||||
}
|
||||
return 'Power Leasing - 电商系统'
|
||||
}
|
||||
|
||||
// 获取页面描述
|
||||
export const getPageDescription = (route) => {
|
||||
if (route.meta && route.meta.description) {
|
||||
return route.meta.description
|
||||
}
|
||||
return 'Power Leasing 电商系统 - 专业的电力设备租赁平台'
|
||||
}
|
||||
|
||||
export default {
|
||||
mainNavigation,
|
||||
breadcrumbConfig,
|
||||
getBreadcrumb,
|
||||
checkRoutePermission,
|
||||
getPageTitle,
|
||||
getPageDescription
|
||||
}
|
||||
87
power_leasing/src/utils/noEmojiGuard.js
Normal file
87
power_leasing/src/utils/noEmojiGuard.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 全局输入表情符号拦截守卫(极简,无侵入)
|
||||
* 作用:拦截所有原生 input/textarea 的输入事件,移除 Emoji,并重新派发 input 事件以同步 v-model
|
||||
* 注意:
|
||||
* - 跳过正在输入法合成阶段(compositionstart ~ compositionend),避免影响中文输入
|
||||
* - 默认对所有可编辑 input/textarea 生效;如需个别放行,可在元素上加 data-allow-emoji="true"
|
||||
*/
|
||||
export const initNoEmojiGuard = () => {
|
||||
if (typeof window === 'undefined') return
|
||||
if (window.__noEmojiGuardInitialized) return
|
||||
window.__noEmojiGuardInitialized = true
|
||||
|
||||
// 覆盖常见 Emoji、旗帜、杂项符号、ZWJ、变体选择符、组合键帽
|
||||
const emojiPattern = /[\u{1F300}-\u{1FAFF}]|[\u{1F1E6}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE0F}]|[\u{200D}]|[\u{20E3}]/gu
|
||||
|
||||
/**
|
||||
* 判断是否是需要拦截的可编辑元素
|
||||
* @param {EventTarget} el 事件目标
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isEditableTarget = (el) => {
|
||||
if (!el || !(el instanceof Element)) return false
|
||||
if (el.getAttribute && el.getAttribute('data-allow-emoji') === 'true') return false
|
||||
const tag = el.tagName
|
||||
if (tag === 'INPUT') {
|
||||
const type = (el.getAttribute('type') || 'text').toLowerCase()
|
||||
// 排除不会产生文本的类型
|
||||
const disallow = ['checkbox', 'radio', 'file', 'hidden', 'button', 'submit', 'reset', 'range', 'color', 'date', 'datetime-local', 'month', 'time', 'week']
|
||||
return disallow.indexOf(type) === -1
|
||||
}
|
||||
if (tag === 'TEXTAREA') return true
|
||||
return false
|
||||
}
|
||||
|
||||
// 记录输入法合成状态
|
||||
const setComposing = (el, composing) => {
|
||||
try { el.__noEmojiComposing = composing } catch (e) {}
|
||||
}
|
||||
const isComposing = (el) => !!(el && el.__noEmojiComposing)
|
||||
|
||||
// 结束合成时做一次清洗
|
||||
document.addEventListener('compositionstart', (e) => {
|
||||
if (!isEditableTarget(e.target)) return
|
||||
setComposing(e.target, true)
|
||||
}, true)
|
||||
document.addEventListener('compositionend', (e) => {
|
||||
if (!isEditableTarget(e.target)) return
|
||||
setComposing(e.target, false)
|
||||
sanitizeAndRedispatch(e.target)
|
||||
}, true)
|
||||
|
||||
// 主输入拦截:捕获阶段尽早处理
|
||||
document.addEventListener('input', (e) => {
|
||||
const target = e.target
|
||||
if (!isEditableTarget(target)) return
|
||||
if (isComposing(target)) return
|
||||
sanitizeAndRedispatch(target)
|
||||
}, true)
|
||||
|
||||
/**
|
||||
* 清洗目标元素的值并在变更时重新派发 input 事件
|
||||
* @param {HTMLInputElement|HTMLTextAreaElement} target
|
||||
*/
|
||||
function sanitizeAndRedispatch(target) {
|
||||
const before = String(target.value ?? '')
|
||||
if (!before) return
|
||||
if (!emojiPattern.test(before)) return
|
||||
const selectionStart = target.selectionStart
|
||||
const selectionEnd = target.selectionEnd
|
||||
const after = before.replace(emojiPattern, '')
|
||||
if (after === before) return
|
||||
target.value = after
|
||||
try {
|
||||
// 重置光标,尽量贴近原位置
|
||||
if (typeof selectionStart === 'number' && typeof selectionEnd === 'number') {
|
||||
const removed = before.length - after.length
|
||||
const nextPos = Math.max(0, selectionStart - removed)
|
||||
target.setSelectionRange(nextPos, nextPos)
|
||||
}
|
||||
} catch (e) {}
|
||||
// 重新派发 input 事件以同步 v-model
|
||||
const evt = new Event('input', { bubbles: true })
|
||||
target.dispatchEvent(evt)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
73
power_leasing/src/utils/productService.js
Normal file
73
power_leasing/src/utils/productService.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @file 商品数据服务(轻量静态数据源)
|
||||
* @description 提供商品列表与详情查询。无需后端即可演示。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Product
|
||||
* @property {string} id - 商品唯一标识
|
||||
* @property {string} title - 商品标题
|
||||
* @property {string} description - 商品描述
|
||||
* @property {number} price - 商品单价(元)
|
||||
* @property {string} image - 商品图片URL(此处使用占位图)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 内置演示商品数据
|
||||
* 使用简短且清晰的字段,满足演示所需
|
||||
* @type {Product[]}
|
||||
*/
|
||||
const products = [
|
||||
{
|
||||
id: 'p1001',
|
||||
title: '新能源充电桩(家用)',
|
||||
description: '7kW 单相,智能预约,支持远程监控。',
|
||||
price: 1299,
|
||||
image: 'https://via.placeholder.com/300x200?text=%E5%85%85%E7%94%B5%E6%A1%A9'
|
||||
},
|
||||
{
|
||||
id: 'p1002',
|
||||
title: '工业电能表',
|
||||
description: '三相四线,远程抄表,Modbus 通信。',
|
||||
price: 899,
|
||||
image: 'https://via.placeholder.com/300x200?text=%E7%94%B5%E8%83%BD%E8%A1%A8'
|
||||
},
|
||||
{
|
||||
id: 'p1003',
|
||||
title: '配电柜(入门版)',
|
||||
description: 'IP54 防护,内置断路器与防雷模块。',
|
||||
price: 5599,
|
||||
image: 'https://via.placeholder.com/300x200?text=%E9%85%8D%E7%94%B5%E6%9F%9C'
|
||||
},
|
||||
{
|
||||
id: 'p1004',
|
||||
title: '工矿照明灯',
|
||||
description: '120W 高亮,耐腐蚀,适配多场景。',
|
||||
price: 329,
|
||||
image: 'https://via.placeholder.com/300x200?text=%E7%85%A7%E6%98%8E%E7%81%AF'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 获取全部商品
|
||||
* @returns {Promise<Product[]>}
|
||||
*/
|
||||
export const listProducts = async () => {
|
||||
return Promise.resolve(products);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取商品
|
||||
* @param {string} productId - 商品ID
|
||||
* @returns {Promise<Product | undefined>}
|
||||
*/
|
||||
export const getProductById = async (productId) => {
|
||||
const product = products.find((p) => p.id === productId);
|
||||
return Promise.resolve(product);
|
||||
}
|
||||
|
||||
export default {
|
||||
listProducts,
|
||||
getProductById
|
||||
}
|
||||
|
||||
458
power_leasing/src/utils/request.js
Normal file
458
power_leasing/src/utils/request.js
Normal file
@@ -0,0 +1,458 @@
|
||||
import axios from 'axios'
|
||||
import errorCode from './errorCode'
|
||||
import { Notification, MessageBox, Message } from 'element-ui'
|
||||
import loadingManager from './loadingManager';
|
||||
import errorNotificationManager from './errorNotificationManager';
|
||||
|
||||
|
||||
const pendingRequestMap = new Map(); //处理Request aborted 错误
|
||||
|
||||
function getRequestKey(config) { //处理Request aborted 错误 生成唯一 key 的函数
|
||||
const { url, method, params, data } = config;
|
||||
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&');
|
||||
}
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
||||
baseURL: process.env.VUE_APP_BASE_API,
|
||||
// 超时
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
// 网络错误相关配置
|
||||
const NETWORK_ERROR_THROTTLE_TIME = 5000; // 错误提示节流时间
|
||||
const RETRY_DELAY = 2000; // 重试间隔时间
|
||||
const MAX_RETRY_TIMES = 3; // 最大重试次数
|
||||
const RETRY_WINDOW = 60000; // 60秒重试窗口
|
||||
let lastNetworkErrorTime = 0; // 上次网络错误提示时间
|
||||
let pendingRequests = new Map();
|
||||
|
||||
|
||||
// 网络状态监听器
|
||||
// 网络状态最后提示时间
|
||||
let lastNetworkStatusTime = {
|
||||
online: 0,
|
||||
offline: 0
|
||||
};
|
||||
|
||||
// 创建一个全局标志,确保每次网络恢复只显示一次提示
|
||||
let networkRecoveryInProgress = false;
|
||||
|
||||
// 网络状态监听器
|
||||
window.addEventListener('online', () => {
|
||||
const now = Date.now();
|
||||
|
||||
// 避免短时间内多次触发
|
||||
if (networkRecoveryInProgress) {
|
||||
console.log('[网络] 网络恢复处理已在进行中,忽略重复事件');
|
||||
return;
|
||||
}
|
||||
|
||||
networkRecoveryInProgress = true;
|
||||
|
||||
// 严格检查是否应该显示提示
|
||||
if (now - lastNetworkStatusTime.online > 30000) { // 30秒内不重复提示
|
||||
lastNetworkStatusTime.online = now;
|
||||
|
||||
try {
|
||||
if (window.vm && window.vm.$message) {
|
||||
// 确保消息只显示一次
|
||||
window.vm.$message({
|
||||
message: window.vm.$i18n.t('home.networkReconnected') || '网络已重新连接,正在恢复数据...',
|
||||
type: 'success',
|
||||
duration: 5000,
|
||||
showClose: true,
|
||||
});
|
||||
console.log('[网络] 显示网络恢复提示, 时间:', new Date().toLocaleTimeString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[网络] 显示网络恢复提示失败:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('[网络] 抑制重复的网络恢复提示, 间隔过短:', now - lastNetworkStatusTime.online + 'ms');
|
||||
}
|
||||
|
||||
// 网络恢复时,重试所有待处理的请求
|
||||
const pendingPromises = [];
|
||||
|
||||
pendingRequests.forEach(async (request, key) => {
|
||||
if (now - request.timestamp <= RETRY_WINDOW) {
|
||||
try {
|
||||
// 获取新的响应数据
|
||||
const response = await service(request.config);
|
||||
pendingPromises.push(response);
|
||||
|
||||
// 执行请求特定的回调
|
||||
if (request.callback && typeof request.callback === 'function') {
|
||||
request.callback(response);
|
||||
}
|
||||
|
||||
// 处理特定类型的请求
|
||||
if (window.vm) {
|
||||
// 处理图表数据请求
|
||||
if (request.config.url.includes('getPoolPower') && response && response.data) {
|
||||
// 触发图表更新事件
|
||||
window.dispatchEvent(new CustomEvent('chart-data-updated', {
|
||||
detail: { type: 'poolPower', data: response.data }
|
||||
}));
|
||||
}
|
||||
else if (request.config.url.includes('getNetPower') && response && response.data) {
|
||||
window.dispatchEvent(new CustomEvent('chart-data-updated', {
|
||||
detail: { type: 'netPower', data: response.data }
|
||||
}));
|
||||
}
|
||||
else if (request.config.url.includes('getBlockInfo') && response && response.rows) {
|
||||
window.dispatchEvent(new CustomEvent('chart-data-updated', {
|
||||
detail: { type: 'blockInfo', data: response.rows }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pendingRequests.delete(key);
|
||||
} catch (error) {
|
||||
console.error('重试请求失败:', error);
|
||||
pendingRequests.delete(key);
|
||||
}
|
||||
} else {
|
||||
pendingRequests.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有请求完成
|
||||
Promise.allSettled(pendingPromises).then(() => {
|
||||
// 重置所有 loading 状态
|
||||
if (loadingManager) {
|
||||
loadingManager.resetAllLoadingStates();
|
||||
}
|
||||
|
||||
// 手动重置一些关键的 loading 状态
|
||||
if (window.vm) {
|
||||
// 常见的加载状态
|
||||
const commonLoadingProps = [
|
||||
'minerChartLoading', 'reportBlockLoading', 'apiPageLoading',
|
||||
'MiningLoading', 'miniLoading', 'bthLoading', 'editLoading'
|
||||
];
|
||||
|
||||
commonLoadingProps.forEach(prop => {
|
||||
if (typeof window.vm[prop] !== 'undefined') {
|
||||
window.vm[prop] = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 重置所有以Loading结尾的状态
|
||||
Object.keys(window.vm).forEach(key => {
|
||||
if (key.endsWith('Loading')) {
|
||||
window.vm[key] = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 触发网络重试完成事件
|
||||
window.dispatchEvent(new CustomEvent('network-retry-complete'));
|
||||
|
||||
// 重置网络恢复标志
|
||||
setTimeout(() => {
|
||||
networkRecoveryInProgress = false;
|
||||
}, 5000); // 5秒后允许再次处理网络恢复
|
||||
});
|
||||
});
|
||||
|
||||
// 使用错误提示管理器控制网络断开提示
|
||||
window.addEventListener('offline', () => {
|
||||
if (window.vm && window.vm.$message && errorNotificationManager.canShowError('networkOffline')) {
|
||||
window.vm.$message({
|
||||
message: window.vm.$i18n.t('home.networkOffline') || '网络连接已断开,系统将在恢复连接后自动重试',
|
||||
type: 'error',
|
||||
duration: 5000,
|
||||
showClose: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
service.defaults.retry = 2;// 重试次数
|
||||
service.defaults.retryDelay = 2000;
|
||||
service.defaults.shouldRetry = (error) => {
|
||||
// 只有网络错误或超时错误才进行重试
|
||||
return error.message === "Network Error" || error.message.includes("timeout");
|
||||
};
|
||||
|
||||
localStorage.setItem('superReportError', "")
|
||||
let superReportError = localStorage.getItem('superReportError')
|
||||
window.addEventListener("setItem", () => {
|
||||
superReportError = localStorage.getItem('superReportError')
|
||||
});
|
||||
|
||||
// request拦截器
|
||||
service.interceptors.request.use(config => {
|
||||
superReportError = ""
|
||||
// retryCount =0
|
||||
localStorage.setItem('superReportError', "")
|
||||
// 是否需要设置 token
|
||||
let token
|
||||
try {
|
||||
token = JSON.parse(localStorage.getItem('token'))
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
if (token) {
|
||||
config.headers['Authorization'] = token
|
||||
}
|
||||
|
||||
console.log(token,"if就覅飞机飞机");
|
||||
|
||||
if (config.method == 'get' && config.data) {
|
||||
config.params = config.data
|
||||
}
|
||||
// get请求映射params参数
|
||||
if (config.method === 'get' && config.params) {
|
||||
let url = config.url + '?';
|
||||
for (const propName of Object.keys(config.params)) {
|
||||
const value = config.params[propName];
|
||||
var part = encodeURIComponent(propName) + "=";
|
||||
if (value !== null && typeof (value) !== "undefined") {
|
||||
if (typeof value === 'object') {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (value[key] !== null && typeof (value[key]) !== 'undefined') {
|
||||
let params = propName + '[' + key + ']';
|
||||
let subPart = encodeURIComponent(params) + '=';
|
||||
url += subPart + encodeURIComponent(value[key]) + '&';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
url += part + encodeURIComponent(value) + "&";
|
||||
}
|
||||
}
|
||||
}
|
||||
url = url.slice(0, -1);
|
||||
config.params = {};
|
||||
config.url = url;
|
||||
}
|
||||
|
||||
// 生成请求唯一key 处理Request aborted 错误
|
||||
const requestKey = getRequestKey(config);
|
||||
|
||||
// 如果有相同请求,先取消 处理Request aborted 错误
|
||||
if (pendingRequestMap.has(requestKey)) {
|
||||
const cancel = pendingRequestMap.get(requestKey);
|
||||
cancel(); // 取消上一次请求
|
||||
pendingRequestMap.delete(requestKey);
|
||||
}
|
||||
|
||||
// 创建新的CancelToken 处理Request aborted 错误
|
||||
config.cancelToken = new axios.CancelToken(cancel => {
|
||||
pendingRequestMap.set(requestKey, cancel);
|
||||
});
|
||||
|
||||
return config
|
||||
}, error => {
|
||||
Promise.reject(error)
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(res => {
|
||||
|
||||
// 请求完成后移除
|
||||
const requestKey = getRequestKey(res.config);
|
||||
pendingRequestMap.delete(requestKey);
|
||||
// 未设置状态码则默认成功状态
|
||||
const code = res.data.code || 200;
|
||||
// 获取错误信息
|
||||
const msg = errorCode[code] || res.data.msg || errorCode['default']
|
||||
if (code === 421) {
|
||||
localStorage.setItem('cs_disconnect_all', Date.now().toString()); //告知客服页面断开连接
|
||||
localStorage.removeItem('token')
|
||||
// 系统状态已过期,请重新点击SUPPORT按钮进入
|
||||
superReportError = localStorage.getItem('superReportError')
|
||||
if (!superReportError) {
|
||||
superReportError = 421
|
||||
localStorage.setItem('superReportError', superReportError)
|
||||
MessageBox.confirm(window.vm.$i18n.t(`user.loginExpired`), window.vm.$i18n.t(`user.overduePrompt`), {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: window.vm.$i18n.t(`user.login`),
|
||||
cancelButtonText: window.vm.$i18n.t(`user.Home`),
|
||||
// showCancelButton: false, // 隐藏取消按钮
|
||||
closeOnClickModal: false, // 点击空白处不关闭对话框
|
||||
showClose: false, // 隐藏关闭按钮
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
window.vm.$router.push(`/${window.vm.$i18n.locale}/login`)
|
||||
localStorage.removeItem('token')
|
||||
}).catch(() => {
|
||||
window.vm.$router.push(`/${window.vm.$i18n.locale}/`)
|
||||
localStorage.removeItem('token')
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
return Promise.reject('登录状态已过期')
|
||||
} else if (code >= 500 && !superReportError) {
|
||||
superReportError = 500
|
||||
localStorage.setItem('superReportError', superReportError)
|
||||
Message({
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: msg,
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
// throw msg; // 抛出错误,中断请求链并触发后续的错误处理逻辑
|
||||
// return Promise.reject(new Error(msg))
|
||||
} else if (code !== 200) {
|
||||
|
||||
|
||||
|
||||
Notification.error({
|
||||
title: msg
|
||||
})
|
||||
return Promise.reject('error')
|
||||
|
||||
} else {
|
||||
|
||||
return res.data
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
error => {
|
||||
|
||||
// 主动取消的请求,直接忽略,不提示
|
||||
if (
|
||||
error.code === 'ERR_CANCELED' ||
|
||||
(error.message && error.message.includes('canceled')) ||
|
||||
error.message?.includes('Request aborted')
|
||||
) {
|
||||
// 静默处理,不提示,不冒泡
|
||||
return new Promise(() => {}); // 返回pending Promise,阻止控制台报错
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 请求异常也要移除 处理Request aborted 错误
|
||||
if (error.config) {
|
||||
const requestKey = getRequestKey(error.config);
|
||||
pendingRequestMap.delete(requestKey);
|
||||
}
|
||||
|
||||
|
||||
let { message } = error;
|
||||
if (message == "Network Error" || message.includes("timeout")) {
|
||||
if (!navigator.onLine) {
|
||||
// 断网状态,添加到重试队列
|
||||
const requestKey = JSON.stringify({
|
||||
url: error.config.url,
|
||||
method: error.config.method,
|
||||
params: error.config.params,
|
||||
data: error.config.data
|
||||
});
|
||||
|
||||
// 根据URL确定请求类型并记录回调
|
||||
let callback = null;
|
||||
if (error.config.url.includes('getPoolPower')) {
|
||||
callback = (data) => {
|
||||
if (window.vm) {
|
||||
// 清除loading状态
|
||||
window.vm.minerChartLoading = false;
|
||||
}
|
||||
};
|
||||
} else if (error.config.url.includes('getBlockInfo')) {
|
||||
callback = (data) => {
|
||||
if (window.vm) {
|
||||
window.vm.reportBlockLoading = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!pendingRequests.has(requestKey)) {
|
||||
pendingRequests.set(requestKey, {
|
||||
config: error.config,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0,
|
||||
callback: callback
|
||||
});
|
||||
|
||||
console.log('请求已加入断网重连队列:', error.config.url);
|
||||
}
|
||||
} else {
|
||||
// 网络已连接,但请求失败,尝试重试
|
||||
// 确保 config 中有 __retryCount 字段
|
||||
error.config.__retryCount = error.config.__retryCount || 0;
|
||||
|
||||
// 判断是否可以重试
|
||||
if (error.config.__retryCount < service.defaults.retry && service.defaults.shouldRetry(error)) {
|
||||
// 增加重试计数
|
||||
error.config.__retryCount += 1;
|
||||
|
||||
console.log(`[请求重试] ${error.config.url} - 第 ${error.config.__retryCount} 次重试`);
|
||||
|
||||
// 创建新的Promise等待一段时间后重试
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(service(error.config));
|
||||
}, service.defaults.retryDelay);
|
||||
});
|
||||
}
|
||||
|
||||
// 达到最大重试次数,不再重试
|
||||
console.log(`[请求失败] ${error.config.url} - 已达到最大重试次数`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!superReportError) {
|
||||
superReportError = "error"
|
||||
localStorage.setItem('superReportError', superReportError)
|
||||
//使用错误提示管理器errorNotificationManager
|
||||
if (errorNotificationManager.canShowError(message)) {
|
||||
if (message == "Network Error") {
|
||||
Message({
|
||||
message: window.vm.$i18n.t(`home.NetworkError`),
|
||||
type: 'error',
|
||||
duration: 4 * 1000,
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
else if (message.includes("timeout")) {
|
||||
Message({
|
||||
message: window.vm.$i18n.t(`home.requestTimeout`),
|
||||
type: 'error',
|
||||
duration: 5 * 1000,
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
else if (message.includes("Request failed with status code")) {
|
||||
Message({
|
||||
message: "系统接口" + message.substr(message.length - 3) + "异常",
|
||||
type: 'error',
|
||||
duration: 5 * 1000,
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
Message({
|
||||
message: message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000,
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 避免完全不提示,可以在控制台记录被抑制的错误
|
||||
console.log('[错误提示] 已抑制重复错误:', message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return Promise.reject(error)
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
export default service
|
||||
200
power_leasing/src/utils/routeTest.js
Normal file
200
power_leasing/src/utils/routeTest.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @file 路由测试工具
|
||||
* @description 用于验证所有路由配置是否正确,便于调试
|
||||
*/
|
||||
|
||||
import { mainRoutes } from '../router/routes'
|
||||
|
||||
/**
|
||||
* 测试路由配置
|
||||
*/
|
||||
export const testRoutes = () => {
|
||||
console.log('🔍 开始测试路由配置...')
|
||||
|
||||
const results = {
|
||||
total: 0,
|
||||
valid: 0,
|
||||
invalid: 0,
|
||||
errors: []
|
||||
}
|
||||
|
||||
// 测试主路由
|
||||
mainRoutes.forEach((route, index) => {
|
||||
results.total++
|
||||
console.log(`\n📋 测试路由 ${index + 1}: ${route.name || '未命名'}`)
|
||||
|
||||
try {
|
||||
// 检查路径
|
||||
if (!route.path) {
|
||||
throw new Error('路由缺少 path 属性')
|
||||
}
|
||||
console.log(`✅ 路径: ${route.path}`)
|
||||
|
||||
// 检查组件
|
||||
if (route.component) {
|
||||
console.log(`✅ 组件: ${typeof route.component}`)
|
||||
} else if (route.redirect) {
|
||||
console.log(`✅ 重定向: ${route.redirect}`)
|
||||
} else {
|
||||
throw new Error('路由缺少 component 或 redirect 属性')
|
||||
}
|
||||
|
||||
// 检查子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
console.log(`✅ 子路由数量: ${route.children.length}`)
|
||||
|
||||
route.children.forEach((child, childIndex) => {
|
||||
console.log(` 📱 子路由 ${childIndex + 1}: ${child.name || '未命名'}`)
|
||||
console.log(` 路径: ${child.path}`)
|
||||
console.log(` 组件: ${typeof child.component}`)
|
||||
|
||||
if (child.meta) {
|
||||
console.log(` 标题: ${child.meta.title || '无标题'}`)
|
||||
console.log(` 权限: ${child.meta.allAuthority?.join(', ') || '无权限要求'}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
results.valid++
|
||||
|
||||
} catch (error) {
|
||||
results.invalid++
|
||||
results.errors.push({
|
||||
route: route.name || route.path,
|
||||
error: error.message
|
||||
})
|
||||
console.error(`❌ 错误: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 输出测试结果
|
||||
console.log('\n📊 路由测试结果:')
|
||||
console.log(`总路由数: ${results.total}`)
|
||||
console.log(`有效路由: ${results.valid}`)
|
||||
console.log(`无效路由: ${results.invalid}`)
|
||||
|
||||
if (results.errors.length > 0) {
|
||||
console.log('\n❌ 发现的问题:')
|
||||
results.errors.forEach((error, index) => {
|
||||
console.log(`${index + 1}. ${error.route}: ${error.error}`)
|
||||
})
|
||||
} else {
|
||||
console.log('\n🎉 所有路由配置正确!')
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试路由导航
|
||||
*/
|
||||
export const testNavigation = () => {
|
||||
console.log('\n🧭 测试导航配置...')
|
||||
|
||||
const testPaths = [
|
||||
'/productList',
|
||||
'/product/p1001',
|
||||
'/cart',
|
||||
'/checkout'
|
||||
]
|
||||
|
||||
testPaths.forEach(path => {
|
||||
console.log(`\n🔗 测试路径: ${path}`)
|
||||
|
||||
// 查找匹配的路由
|
||||
const matchedRoute = findRouteByPath(path, mainRoutes)
|
||||
|
||||
if (matchedRoute) {
|
||||
console.log(`✅ 找到路由: ${matchedRoute.name || '未命名'}`)
|
||||
if (matchedRoute.meta) {
|
||||
console.log(` 标题: ${matchedRoute.meta.title}`)
|
||||
console.log(` 描述: ${matchedRoute.meta.description}`)
|
||||
}
|
||||
} else {
|
||||
console.log(`❌ 未找到匹配的路由`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径查找路由
|
||||
*/
|
||||
const findRouteByPath = (path, routes) => {
|
||||
for (const route of routes) {
|
||||
// 检查当前路由
|
||||
if (route.path === path) {
|
||||
return route
|
||||
}
|
||||
|
||||
// 检查子路由
|
||||
if (route.children) {
|
||||
for (const child of route.children) {
|
||||
// 处理动态路由
|
||||
if (child.path.includes(':')) {
|
||||
const pattern = new RegExp('^' + child.path.replace(/:[^/]+/g, '[^/]+') + '$')
|
||||
if (pattern.test(path)) {
|
||||
return child
|
||||
}
|
||||
} else if (child.path === path) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用路径
|
||||
*/
|
||||
export const getAllPaths = () => {
|
||||
const paths = []
|
||||
|
||||
const extractPaths = (routes) => {
|
||||
routes.forEach(route => {
|
||||
if (route.path && !route.redirect) {
|
||||
paths.push(route.path)
|
||||
}
|
||||
if (route.children) {
|
||||
extractPaths(route.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
extractPaths(mainRoutes)
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行完整测试
|
||||
*/
|
||||
export const runFullTest = () => {
|
||||
console.log('🚀 开始运行完整路由测试...\n')
|
||||
|
||||
// 测试路由配置
|
||||
const routeResults = testRoutes()
|
||||
|
||||
// 测试导航
|
||||
testNavigation()
|
||||
|
||||
// 获取所有路径
|
||||
const allPaths = getAllPaths()
|
||||
console.log('\n📋 所有可用路径:')
|
||||
allPaths.forEach((path, index) => {
|
||||
console.log(`${index + 1}. ${path}`)
|
||||
})
|
||||
|
||||
console.log('\n✨ 路由测试完成!')
|
||||
|
||||
return {
|
||||
routeResults,
|
||||
allPaths
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
testRoutes,
|
||||
testNavigation,
|
||||
getAllPaths,
|
||||
runFullTest
|
||||
}
|
||||
5
power_leasing/src/views/AboutView.vue
Normal file
5
power_leasing/src/views/AboutView.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
18
power_leasing/src/views/HomeView.vue
Normal file
18
power_leasing/src/views/HomeView.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<img alt="Vue logo" src="../assets/logo.png">
|
||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
export default {
|
||||
name: 'HomeView',
|
||||
components: {
|
||||
HelloWorld
|
||||
}
|
||||
}
|
||||
</script>
|
||||
223
power_leasing/src/views/account/OrderList.vue
Normal file
223
power_leasing/src/views/account/OrderList.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div v-loading="payLoading">
|
||||
<div v-if="!safeItems.length" class="empty">{{ emptyText }}</div>
|
||||
<el-table v-else :data="safeItems" border :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
|
||||
<el-table-column type="expand" width="46">
|
||||
<template #default="outer">
|
||||
<el-table :data="outer.row.orderItemDtoList || []" size="small" border :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }" row-key="productMachineId">
|
||||
<el-table-column prop="productMachineId" label="机器ID" min-width="120" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" />
|
||||
<el-table-column prop="payCoin" label="币种" min-width="100" />
|
||||
<el-table-column prop="address" label="收款地址" min-width="240" />
|
||||
<el-table-column prop="leaseTime" label="租赁天数" min-width="100" />
|
||||
<el-table-column prop="price" label="单价(USDT)" min-width="240" />
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="订单号" min-width="220">
|
||||
<template #default="scope"><span class="value mono">{{ scope.row && scope.row.orderNumber || '—' }}</span></template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
<template #default="scope">{{ formatDateTime(scope.row && scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品数" min-width="100">
|
||||
<template #default="scope">{{ Array.isArray(scope.row && scope.row.orderItemDtoList) ? scope.row.orderItemDtoList.length : 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总金额(USDT)" min-width="140">
|
||||
<template #default="scope"><span class="value strong">{{ (scope.row && scope.row.totalPrice) != null ? scope.row.totalPrice : '—' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已支付金额(USDT)" min-width="140">
|
||||
<template #default="scope"><span class="value strong">{{ (scope.row && scope.row.payAmount) != null ? scope.row.payAmount : '—' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="待支付金额(USDT)" min-width="140">
|
||||
<template #default="scope"><span class="value strong">{{ (scope.row && scope.row.noPayAmount) != null ? scope.row.noPayAmount : '—' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="280" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="mini" @click="handleGoDetail(scope.row)" style="margin-right:8px;">详情</el-button>
|
||||
<template v-if="shouldShowActions(scope.row)">
|
||||
<el-button type="primary" size="mini" @click="handleCheckout(scope.row)">去结算</el-button>
|
||||
<!-- <el-button type="danger" size="mini" style="margin-left:8px;" @click="handleCancel(scope.row)">取消订单</el-button> -->
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 修改功能之前 跟购物车弹窗一样
|
||||
<el-dialog :visible.sync="orderDialog.visible" width="520px" title="请扫码支付">
|
||||
<div style="text-align:left; margin-bottom:12px; color:#666;">
|
||||
<div style="margin-bottom:6px;">支付币种:<b>{{ orderDialog.coin }}</b></div>
|
||||
<div style="margin-bottom:6px;">支付金额(USDT):<b class="value strong">{{ orderDialog.amount }}</b></div>
|
||||
<div style="word-break:break-all;">收款地址:<code>{{ orderDialog.address }}</code></div>
|
||||
</div>
|
||||
<div style="text-align:left;">
|
||||
<img v-if="orderDialog.qrContent" :src="orderDialog.qrContent" alt="支付二维码" style="width:240px;height:240px;" />
|
||||
<div v-else style="color:#666;">未返回支付二维码</div>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="orderDialog.visible=false">关闭</el-button>
|
||||
</span>
|
||||
</el-dialog> -->
|
||||
|
||||
|
||||
|
||||
<el-dialog :visible.sync="dialogVisible" width="520px" title="请扫码支付">
|
||||
<div style="text-align:left; margin-bottom:12px; color:#666;">
|
||||
<div style="margin-bottom:6px;">总金额(USDT):<b>{{ paymentDialog.totalPrice }}</b></div>
|
||||
<div style="margin-bottom:6px;">已支付金额(USDT):<b class="value strong">{{ paymentDialog.payAmount }}</b></div>
|
||||
<div style="margin-bottom:6px;">待支付金额(USDT):<b class="value strong">{{ paymentDialog.noPayAmount }}</b></div>
|
||||
<!-- <div style="word-break:break-all;">收款地址:<code>{{ orderDialog.address }}</code></div> -->
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<img v-if="paymentDialog.img" :src="paymentDialog.img" alt="支付二维码" style="width:180px;height:180px;margin-top: 18px;" />
|
||||
<div v-else style="color:#666;">未返回支付二维码</div>
|
||||
</div>
|
||||
<p style="margin-bottom:6px;color:red;text-align:left">注意:如果已经支付对应金额,不要在重复支付,待系统确认后会自动更新订单状态。因个人原因重复支付导致无法退款,平台不承担任何责任。</p>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible=false">关闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addOrders } from '../../api/order'
|
||||
export default {
|
||||
name: 'OrderList',
|
||||
props: {
|
||||
items: { type: Array, default: () => [] },
|
||||
emptyText: { type: String, default: '暂无数据' },
|
||||
showCheckout: { type: Boolean, default: false },
|
||||
onCancel: { type: Function, default: null }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
payLoading: false,
|
||||
orderDialog: { visible: false, qrContent: '', coin: '', amount: '', address: '' },
|
||||
dialogVisible: false,
|
||||
paymentDialog: { totalPrice: "", payAmount: '', noPayAmount: '', img: '', }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
safeItems() {
|
||||
return Array.isArray(this.items) ? this.items : []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildQrSrc(img) {
|
||||
if (!img) return ''
|
||||
try { const s = String(img).trim(); return s.startsWith('data:') ? s : `data:image/png;base64,${s}` } catch (e) { return '' }
|
||||
},
|
||||
formatDateTime(value) {
|
||||
if (!value) return '—'
|
||||
try { const str = String(value); return str.includes('T') ? str.replace('T', ' ') : str } catch (e) { return String(value) }
|
||||
},
|
||||
async handleCheckout(row) {
|
||||
if (!row) return
|
||||
try {
|
||||
this.payLoading = true
|
||||
this.paymentDialog={
|
||||
totalPrice: row.totalPrice,
|
||||
payAmount: row.payAmount,
|
||||
noPayAmount: row.noPayAmount,
|
||||
img: row.img
|
||||
}
|
||||
|
||||
if (this.paymentDialog.img) {
|
||||
this.paymentDialog.img = this.buildQrSrc(this.paymentDialog.img)
|
||||
this.dialogVisible=true
|
||||
}else{
|
||||
this.$message({
|
||||
message: '未返回支付二维码',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
});
|
||||
|
||||
}
|
||||
// this.dialogVisible=true
|
||||
// const list = Array.isArray(row.orderItemDtoList) ? row.orderItemDtoList : []
|
||||
// const payload = list.map(m => {
|
||||
// const base = {
|
||||
// leaseTime: Number(m.leaseTime || 1),
|
||||
// machineId: m.productMachineId != null ? m.productMachineId : (m.machineId != null ? m.machineId : m.id),
|
||||
// productId: m.productId,
|
||||
// shopId: m.shopId
|
||||
// }
|
||||
// // 尝试从子项或订单行上获取店铺ID并附加
|
||||
// const sid = m.shopId != null ? m.shopId : (row && (row.shopId != null ? row.shopId : row.shopID))
|
||||
// return sid != null ? { ...base, shopId: sid } : base
|
||||
// }).filter(p => p.machineId != null && p.productId != null)
|
||||
// if (!payload.length) { this.$message.warning('该订单没有可支付的机器项'); return }
|
||||
// const res = await addOrders(payload)
|
||||
// if (!res || Number(res.code) !== 200) {
|
||||
// // 全局拦截器已弹错误,这里只记录并中断,避免重复弹窗
|
||||
// console.warn('创建支付订单失败:', res)
|
||||
// return
|
||||
// }
|
||||
// const first = Array.isArray(res.data) && res.data.length ? res.data[0] : null
|
||||
// if (!first || !first.img) { this.$message.error('未返回支付二维码'); return }
|
||||
// this.orderDialog.coin = first.payCoin || ''
|
||||
// this.orderDialog.amount = first.amount != null ? String(first.amount) : ''
|
||||
// this.orderDialog.address = first.payAddress || ''
|
||||
// this.orderDialog.qrContent = this.buildQrSrc(first.img)
|
||||
// this.orderDialog.visible = true
|
||||
} catch (e) {
|
||||
console.log(e,'创建支付订单失败');
|
||||
} finally { this.payLoading = false }
|
||||
},
|
||||
handleGoDetail(row) {
|
||||
const id = row && (row.id != null ? row.id : row.orderId)
|
||||
if (id == null) {
|
||||
this.$message({
|
||||
message: '订单ID缺失',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
});
|
||||
return
|
||||
}
|
||||
try { this.$router.push(`/account/order-detail/${id}`) } catch (e) {
|
||||
this.$message({
|
||||
message: '无法跳转到详情页',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
},
|
||||
handleCancel(row) {
|
||||
if (!row || !this.onCancel) return
|
||||
const id = row.id
|
||||
if (id == null) {
|
||||
this.$message({
|
||||
message: '订单ID缺失',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
});
|
||||
return
|
||||
}
|
||||
this.$confirm('确认取消该订单吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||
.then(() => { try { this.onCancel({ orderId: id }) } catch (e) { void 0 } })
|
||||
.catch(() => { return null })
|
||||
},
|
||||
shouldShowActions(row) {
|
||||
console.log(row,'飞机飞机覅附件s');
|
||||
if (!this.showCheckout) return false
|
||||
const s = Number(row && row.status)
|
||||
console.log(s,'飞机飞机覅附件s');
|
||||
|
||||
// 支持在 待支付(0) 与 支付中(6) 部分已支付(10) 三个状态显示按钮
|
||||
return s === 0 || s === 6 || s === 10
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty { color: #888; padding: 24px; text-align: center; }
|
||||
.value.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; word-break: break-all; }
|
||||
.value.strong { font-weight: 700; color: #e74c3c; }
|
||||
</style>
|
||||
|
||||
|
||||
94
power_leasing/src/views/account/SellerOrders.vue
Normal file
94
power_leasing/src/views/account/SellerOrders.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="orders-page">
|
||||
<h2 class="title">已售出订单</h2>
|
||||
<el-tabs v-model="active" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="订单进行中" name="7">
|
||||
<order-list :items="orders[7]" :show-checkout="false" empty-text="暂无进行中的订单" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="订单已完成" name="8">
|
||||
<order-list :items="orders[8]" :show-checkout="false" empty-text="暂无已完成的订单" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 卖家侧订单列表页(个人中心)
|
||||
* - 与买家 orders.vue 结构一致,接口改为 getOrdersByStatusForSeller
|
||||
* - 标签状态:7 订单进行中;8 订单已完成
|
||||
*/
|
||||
import { getOrdersByStatusForSeller } from '../../api/order'
|
||||
import OrderList from './OrderList.vue'
|
||||
|
||||
export default {
|
||||
name: 'AccountSellerOrders',
|
||||
components: { OrderList },
|
||||
data() {
|
||||
return {
|
||||
active: '7',
|
||||
orders: { 7: [], 8: [] },
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const urlStatus = this.$route && this.$route.query && this.$route.query.status ? String(this.$route.query.status) : null
|
||||
const savedStatus = localStorage.getItem('sellerOrderListActiveTab')
|
||||
const initial = urlStatus || savedStatus || '7'
|
||||
this.active = initial
|
||||
this.fetchOrders(initial)
|
||||
},
|
||||
methods: {
|
||||
handleTabClick(tab) {
|
||||
const name = tab && tab.name ? String(tab.name) : this.active
|
||||
try {
|
||||
localStorage.setItem('sellerOrderListActiveTab', name)
|
||||
} catch (e) {}
|
||||
this.fetchOrders(name)
|
||||
},
|
||||
async fetchOrders(status) {
|
||||
const key = String(status)
|
||||
try {
|
||||
this.loading = true
|
||||
const res = await getOrdersByStatusForSeller({ status: Number(status) })
|
||||
const payload = (res && res.data) != null ? res.data : res
|
||||
const list = Array.isArray(payload)
|
||||
? payload
|
||||
: (Array.isArray(payload && payload.rows) ? payload.rows : [])
|
||||
this.$set(this.orders, key, list)
|
||||
} catch (e) {
|
||||
console.error('获取卖家订单失败', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.orders-page { padding: 12px; }
|
||||
.title { margin: 0 0 12px 0; font-weight: 600; color: #2c3e50; }
|
||||
.empty { color: #888; padding: 24px; text-align: center; }
|
||||
.order-list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.order-card { border: 1px solid #eee; border-radius: 8px; padding: 0; background: #fff; overflow: hidden; }
|
||||
.order-header { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; padding: 12px; cursor: pointer; position: relative; }
|
||||
.order-header:focus { outline: 2px solid #409eff; outline-offset: -2px; }
|
||||
.order-header.is-open { background: #fafafa; }
|
||||
.header-row { display: flex; gap: 8px; line-height: 1.8; align-items: center; }
|
||||
.chevron { position: absolute; right: 12px; top: 12px; width: 10px; height: 10px; border-right: 2px solid #666; border-bottom: 2px solid #666; transform: rotate(-45deg); transition: transform 0.2s ease; }
|
||||
.chevron.chevron-open { transform: rotate(45deg); }
|
||||
.order-details { border-top: 1px solid #eee; padding: 12px; }
|
||||
.machine-list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.machine-card { border: 1px dashed #e2e2e2; border-radius: 6px; padding: 10px; background: #fff; }
|
||||
.row { display: flex; gap: 8px; line-height: 1.8; }
|
||||
.label { color: #666; }
|
||||
.value { color: #333; }
|
||||
.value.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; word-break: break-all; }
|
||||
.value.strong { font-weight: 700; color: #e74c3c; }
|
||||
@media (max-width: 960px) {
|
||||
.order-list { grid-template-columns: 1fr; }
|
||||
.order-header { grid-template-columns: 1fr; }
|
||||
.machine-list { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
220
power_leasing/src/views/account/index.vue
Normal file
220
power_leasing/src/views/account/index.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="account-page">
|
||||
<!-- <div class="account-header">
|
||||
<h1 class="title">个人中心</h1>
|
||||
</div> -->
|
||||
|
||||
<div class="account-layout">
|
||||
<!-- 左侧导航 -->
|
||||
<aside class="sidebar">
|
||||
<nav class="side-nav">
|
||||
<div class="user-info-card" role="region" v-show="userEmail" aria-label="用户信息" tabindex="0">
|
||||
<div class="user-meta">
|
||||
<div class="user-email" :title="userEmail || '未登录'">{{ userEmail || '未登录' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-role">
|
||||
<button>买家相关</button>
|
||||
<button>卖家相关</button>
|
||||
</div>
|
||||
<router-link
|
||||
to="/account/wallet"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>我的钱包</router-link
|
||||
>
|
||||
<!-- <router-link
|
||||
to="/account/shop-new"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>新增店铺</router-link
|
||||
> -->
|
||||
<router-link
|
||||
to="/account/shop-config"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>钱包绑定</router-link
|
||||
>
|
||||
<router-link
|
||||
to="/account/shops"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>我的店铺</router-link
|
||||
>
|
||||
<router-link
|
||||
to="/account/products"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>商品列表</router-link
|
||||
>
|
||||
<router-link
|
||||
to="/account/purchased"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>已购商品</router-link
|
||||
>
|
||||
<router-link
|
||||
to="/account/seller-orders"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>已售出订单</router-link>
|
||||
<router-link
|
||||
to="/account/orders"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>已购买订单列表</router-link>
|
||||
|
||||
<router-link
|
||||
to="/account/rechargeRecord"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>充值记录</router-link>
|
||||
<router-link
|
||||
to="/account/withdrawalHistory"
|
||||
class="side-link"
|
||||
active-class="active"
|
||||
>提现记录</router-link>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<section class="content">
|
||||
<router-view />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AccountPage",
|
||||
data() {
|
||||
return {
|
||||
activeIndex: '1',
|
||||
userEmail: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 计算用户邮箱首字母(用于头像)
|
||||
* @returns {string}
|
||||
*/
|
||||
userInitial() {
|
||||
const email = (this.userEmail || '').trim()
|
||||
return email ? email[0].toUpperCase() : '?'
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const getVal = (key) => {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (raw == null) return null
|
||||
try { return JSON.parse(raw) } catch (e) { return raw }
|
||||
}
|
||||
const val = getVal('userName') || getVal('userEmail') || ''
|
||||
this.userEmail = typeof val === 'string' ? val : String(val)
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.account-page {
|
||||
padding: 20px;
|
||||
}
|
||||
.account-header {
|
||||
background: #fff;
|
||||
/* border: 1px solid #eee; */
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
padding-left: 3vw;
|
||||
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
|
||||
}
|
||||
|
||||
.account-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
min-height: 80vh;
|
||||
}
|
||||
.side-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
/* 用户信息卡片:置于导航最前,展示邮箱与首字母头像 */
|
||||
.user-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #42b983, #67c23a);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
.user-email {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.side-link {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
color: #2c3e50;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.side-link:hover {
|
||||
background: #f6f8fa;
|
||||
}
|
||||
.side-link.active {
|
||||
background: #42b983;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.account-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
517
power_leasing/src/views/account/myShops.vue
Normal file
517
power_leasing/src/views/account/myShops.vue
Normal file
@@ -0,0 +1,517 @@
|
||||
<template>
|
||||
<div class="panel" >
|
||||
<h2 class="panel-title">我的店铺</h2>
|
||||
<div class="panel-body">
|
||||
<el-card v-if="loaded && hasShop" class="shop-card" shadow="hover">
|
||||
<div class="shop-row">
|
||||
<div class="shop-cover">
|
||||
<img :src="shop.image || defaultCover" alt="店铺封面" />
|
||||
</div>
|
||||
<div class="shop-info">
|
||||
<div class="shop-title">
|
||||
<span class="name">{{ shop.name || '未命名店铺' }}</span>
|
||||
<el-tag size="small" :type="shopStateTagType">
|
||||
{{ shopStateText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="desc">{{ shop.description || '这家店还没有描述~' }}</div>
|
||||
<!-- <div class="meta">
|
||||
<span>店铺ID:{{ shop.id || '-' }}</span>
|
||||
<span>可删除:{{ shop.del ? '是' : '否' }}</span>
|
||||
</div> -->
|
||||
<div class="actions">
|
||||
<el-button size="small" type="primary" @click="handleOpenEdit">修改店铺</el-button>
|
||||
<el-button size="small" type="warning" @click="handleToggleShop">
|
||||
{{ shop.state === 2 ? '开启店铺' : '关闭店铺' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete">删除店铺</el-button>
|
||||
<el-button size="small" type="success" @click="handleAddProduct">新增商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 店铺配置表格 -->
|
||||
<!-- <el-card v-if="loaded && hasShop" class="shop-config-card" shadow="never" style="margin-top: 16px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>店铺配置</span>
|
||||
</div>
|
||||
<el-table :data="shopConfigs" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="商品" width="120">
|
||||
<template slot-scope="scope">{{ scope.row.productId === 0 ? '所有商品' : scope.row.productId }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payType" label="币种类型" width="120">
|
||||
<template slot-scope="scope">{{ scope.row.payType === 1 ? '稳定币' : '虚拟币' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payCoin" label="支付币种" width="140" />
|
||||
<el-table-column prop="payAddress" label="收款钱包地址" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" @click="handleEditConfig(scope.row)">修改</el-button>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button type="text" style="color:#e74c3c" @click="handleDeleteConfig(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card> -->
|
||||
|
||||
<div v-else-if="loaded && !hasShop" class="no-shop">
|
||||
<el-empty description="暂无店铺">
|
||||
<el-button type="primary" @click="handleGoNew">新建店铺</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="正在加载店铺信息..." />
|
||||
|
||||
<!-- 修改店铺弹窗 -->
|
||||
<el-dialog title="修改店铺" :visible.sync="visibleEdit" width="520px">
|
||||
<div class="row">
|
||||
<label class="label">店铺名称</label>
|
||||
<el-input v-model="editForm.name" placeholder="请输入店铺名称" :maxlength="30" show-word-limit />
|
||||
</div>
|
||||
<!-- <div class="row">
|
||||
<label class="label">店铺封面</label>
|
||||
<el-input v-model="editForm.image" placeholder="请输入图片地址" />
|
||||
</div> -->
|
||||
<div class="row">
|
||||
<label class="label">店铺描述</label>
|
||||
<el-input type="textarea" :rows="3" v-model="editForm.description" placeholder="请输入描述" :maxlength="300" show-word-limit />
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="visibleEdit=false">取消</el-button>
|
||||
<el-button type="primary" @click="submitEdit">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<!-- 修改店铺配置弹窗 -->
|
||||
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
|
||||
<div class="row">
|
||||
<label class="label">适用商品</label>
|
||||
<el-select v-model="configForm.productId" placeholder="请选择商品">
|
||||
<el-option :value="0" label="所有商品" />
|
||||
<el-option v-for="p in productOptions" :key="p.id" :value="p.id" :label="`${p.id} - ${p.name}`" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="label">收款地址</label>
|
||||
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="label">币种类型</label>
|
||||
<el-radio-group v-model="configForm.payType">
|
||||
<el-radio :label="0">虚拟币</el-radio>
|
||||
<el-radio :label="1">稳定币</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="label">支付币种</label>
|
||||
|
||||
<el-select
|
||||
class="input"
|
||||
size="middle"
|
||||
ref="screen"
|
||||
v-model="configForm.payCoin"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in coinOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div style="display: flex; align-items: center">
|
||||
<img :src="item.imgUrl" style="float: left; width: 20px" />
|
||||
<span style="float: left; margin-left: 5px">
|
||||
{{ item.label }}</span
|
||||
>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="visibleConfigEdit=false">取消</el-button>
|
||||
<el-button type="primary" @click="submitConfigEdit">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getMyShop, updateShop, deleteShop, queryShop, closeShop, getShopConfig ,updateShopConfig,deleteShopConfig} from '@/api/shops'
|
||||
|
||||
import { coinList } from '@/utils/coinList'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'AccountMyShops',
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
defaultCover: 'https://dummyimage.com/120x120/eee/999.png&text=Shop',
|
||||
shop: {
|
||||
id: 0,
|
||||
name: '',
|
||||
image: '',
|
||||
description: '',
|
||||
del: true,
|
||||
state: 0
|
||||
},
|
||||
visibleEdit: false,
|
||||
editForm: { id: '', name: '', image: '', description: '' },
|
||||
// 店铺配置列表
|
||||
shopConfigs: [],
|
||||
visibleConfigEdit: false,
|
||||
configForm: { id: '', payAddress: '', payCoin: '', payType: 0, productId: 0 },
|
||||
productOptions: [],
|
||||
coinOptions: coinList || [],
|
||||
shopLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shopStateText() {
|
||||
// 0 待审核 1 审核通过(店铺开启) 2 店铺关闭
|
||||
if (this.shop.state === 0) return '待审核'
|
||||
if (this.shop.state === 1) return '店铺开启'
|
||||
if (this.shop.state === 2) return '店铺关闭'
|
||||
return '未知状态'
|
||||
},
|
||||
shopStateTagType() {
|
||||
// 标签配色:待审核=warning,开启=success,关闭=info
|
||||
if (this.shop.state === 0) return 'warning'
|
||||
if (this.shop.state === 1) return 'success'
|
||||
if (this.shop.state === 2) return 'info'
|
||||
return 'info'
|
||||
},
|
||||
hasShop() {
|
||||
return !!(this.shop && Number(this.shop.id) > 0)
|
||||
},
|
||||
canCreateShop() {
|
||||
return !this.hasShop
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchMyShop()
|
||||
},
|
||||
methods: {
|
||||
// 简单的emoji检测:覆盖常见表情平面与符号范围
|
||||
hasEmoji(str) {
|
||||
if (!str || typeof str !== 'string') return false
|
||||
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u2600-\u27BF]/u
|
||||
return emojiRegex.test(str)
|
||||
},
|
||||
/**
|
||||
* 重置店铺状态为无店铺状态
|
||||
*/
|
||||
resetShopState() {
|
||||
this.shop = {
|
||||
id: 0,
|
||||
name: '',
|
||||
image: '',
|
||||
description: '',
|
||||
del: true,
|
||||
state: 0
|
||||
}
|
||||
this.shopConfigs = []
|
||||
},
|
||||
|
||||
async fetchMyShop() {
|
||||
|
||||
try {
|
||||
const res = await getMyShop()
|
||||
// 预期格式:{"code":0,"data":{"del":true,"description":"","id":0,"image":"","name":"","state":0},"msg":""}
|
||||
if (res && (res.code === 0 || res.code === 200) && res.data) {
|
||||
this.shop = {
|
||||
id: res.data.id,
|
||||
name: res.data.name,
|
||||
image: res.data.image,
|
||||
description: res.data.description,
|
||||
del: !!res.data.del,
|
||||
state: Number(res.data.state || 0)
|
||||
}
|
||||
// 同步加载店铺配置
|
||||
this.fetchShopConfigs(res.data.id)
|
||||
} else {
|
||||
// 当接口返回错误或没有数据时,重置店铺状态
|
||||
this.resetShopState()
|
||||
if (res && res.msg) {
|
||||
console.warn('获取店铺数据失败:', res.msg)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺信息失败:', error)
|
||||
// 当接口报错(如500错误)时,重置店铺状态
|
||||
this.resetShopState()
|
||||
} finally {
|
||||
this.loaded = true
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
async fetchShopConfigs(shopId) {
|
||||
// 如果店铺ID无效,直接清空配置
|
||||
if (!shopId || shopId <= 0) {
|
||||
this.shopConfigs = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getShopConfig(shopId)
|
||||
if (res && (res.code === 0 || res.code === 200) && Array.isArray(res.data)) {
|
||||
this.shopConfigs = res.data
|
||||
} else {
|
||||
this.shopConfigs = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('获取店铺配置失败:', e)
|
||||
this.shopConfigs = []
|
||||
}
|
||||
},
|
||||
async updateShopConfig(params) {
|
||||
const res = await updateShopConfig(params)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message.success('保存成功')
|
||||
this.visibleConfigEdit = false
|
||||
this.fetchShopConfigs(this.shop.id)
|
||||
} else {
|
||||
this.$message.error(res && res.msg ? res.msg : '保存失败')
|
||||
}
|
||||
},
|
||||
async deleteShopConfig(params) {
|
||||
const res = await deleteShopConfig(params)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message.success('删除成功')
|
||||
this.fetchShopConfigs(this.shop.id)
|
||||
}
|
||||
},
|
||||
handleEditConfig(row) {
|
||||
this.configForm = { ...row }
|
||||
this.visibleConfigEdit = true
|
||||
|
||||
},
|
||||
async handleDeleteConfig(row) {
|
||||
this.deleteShopConfig({id:row.id})
|
||||
},
|
||||
|
||||
submitConfigEdit() {
|
||||
this.updateShopConfig(this.configForm)
|
||||
|
||||
},
|
||||
async handleOpenEdit() {
|
||||
try {
|
||||
// 先打开弹窗,提供更快的视觉反馈
|
||||
this.visibleEdit = true
|
||||
// 查询最新店铺详情
|
||||
const res = await queryShop({ id: this.shop.id })
|
||||
if (res && (res.code === 0 || res.code === 200) && res.data) {
|
||||
this.editForm = {
|
||||
id: res.data.id,
|
||||
name: res.data.name,
|
||||
image: res.data.image,
|
||||
description: res.data.description
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
// 回退到当前展示的数据
|
||||
this.editForm = {
|
||||
id: this.shop.id,
|
||||
name: this.shop.name,
|
||||
image: this.shop.image,
|
||||
description: this.shop.description
|
||||
}
|
||||
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
|
||||
}
|
||||
} catch (error) {
|
||||
// 出错时回退到当前展示的数据
|
||||
this.editForm = {
|
||||
id: this.shop.id,
|
||||
name: this.shop.name,
|
||||
image: this.shop.image,
|
||||
description: this.shop.description
|
||||
}
|
||||
console.error('查询店铺详情失败:', error)
|
||||
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 提交店铺修改
|
||||
* 规则:允许输入空格,但不允许内容为“全是空格”
|
||||
*/
|
||||
async submitEdit() {
|
||||
try {
|
||||
const { name, image, description } = this.editForm
|
||||
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
|
||||
if (isOnlySpaces(name)) {
|
||||
this.$message.error('店铺名称不能全是空格')
|
||||
return
|
||||
}
|
||||
if (!name) {
|
||||
this.$message.error('店铺名称不能为空')
|
||||
return
|
||||
}
|
||||
if (this.hasEmoji(name)) {
|
||||
this.$message.warning('店铺名称不能包含表情符号')
|
||||
return
|
||||
}
|
||||
if (isOnlySpaces(image)) {
|
||||
this.$message.error('店铺封面不能全是空格')
|
||||
return
|
||||
}
|
||||
if (isOnlySpaces(description)) {
|
||||
this.$message.error('店铺描述不能全是空格')
|
||||
return
|
||||
}
|
||||
// 长度限制:名称≤30,描述≤300
|
||||
if (name && name.length > 30) {
|
||||
this.$message.warning('店铺名称不能超过30个字符')
|
||||
return
|
||||
}
|
||||
if (description && description.length > 300) {
|
||||
this.$message.warning('店铺描述不能超过300个字符')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const payload = { ...this.editForm }
|
||||
const res = await updateShop(payload)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message({
|
||||
message: '已保存',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
this.visibleEdit = false
|
||||
this.fetchMyShop()
|
||||
} else {
|
||||
this.$message({
|
||||
message: res.msg || '保存失败',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新店铺失败:', error)
|
||||
console.log('更新店铺失败,请稍后重试')
|
||||
}
|
||||
},
|
||||
async handleDelete() {
|
||||
try {
|
||||
await this.$confirm('确定删除该店铺吗?此操作不可恢复', '提示', { type: 'warning' })
|
||||
const res = await deleteShop(this.shop.id)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message({
|
||||
message: '删除成功',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
// 删除成功后,先重置店铺状态,然后尝试重新获取
|
||||
this.resetShopState()
|
||||
this.loaded = false
|
||||
// 延迟一下再尝试获取,给服务器时间处理删除操作
|
||||
setTimeout(() => {
|
||||
this.fetchMyShop()
|
||||
}, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
// 用户取消
|
||||
}
|
||||
},
|
||||
async handleToggleShop() {
|
||||
try {
|
||||
const isClosed = this.shop.state === 2
|
||||
const confirmMsg = isClosed ? '确定开启店铺吗?' : '确定关闭该店铺吗?关闭后用户将无法访问'
|
||||
await this.$confirm(confirmMsg, '提示', { type: 'warning' })
|
||||
const res = await closeShop(this.shop.id)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message({
|
||||
message: isClosed ? '店铺已开启' : '店铺已关闭',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
this.fetchMyShop()
|
||||
} else {
|
||||
// this.$message.error(res && res.msg ? res.msg : '操作失败')
|
||||
console.log(`操作失败`);
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
// 用户取消
|
||||
}
|
||||
},
|
||||
|
||||
handleGoNew() {
|
||||
if (!this.canCreateShop) {
|
||||
this.$message({
|
||||
message: '每个用户仅允许一个店铺,无法新建',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$router.push('/account/shop-new')
|
||||
},
|
||||
|
||||
/**
|
||||
* 跳转到新增商品页面
|
||||
*/
|
||||
handleAddProduct() {
|
||||
if (!this.hasShop) {
|
||||
this.$message({
|
||||
message: '请先创建店铺',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
})
|
||||
return
|
||||
}
|
||||
// 跳转到新增商品页面,并传递店铺ID
|
||||
this.$router.push({
|
||||
path: '/account/product-new',
|
||||
query: { shopId: this.shop.id }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel-title { margin: 0 0 12px 0; font-size: 18px; font-weight: 700; }
|
||||
.shop-card { border-radius: 8px; }
|
||||
.shop-row { display: grid; grid-template-columns: 120px 1fr; gap: 16px; align-items: center; }
|
||||
.shop-cover img { width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; }
|
||||
.shop-info { display: flex; flex-direction: column; gap: 8px; }
|
||||
.shop-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 16px; }
|
||||
.desc { color: #666; }
|
||||
.meta { color: #999; display: flex; gap: 16px; font-size: 12px; }
|
||||
.actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局弹窗宽度微调(仅当前页面生效)*/
|
||||
.el-dialog__body .row { margin-bottom: 12px; }
|
||||
|
||||
/* 弹窗表单统一对齐与留白优化 */
|
||||
.el-dialog__body .row {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
column-gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.el-dialog__body .row .el-radio-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding-left: 0; /* 与输入框左边缘对齐 */
|
||||
margin-left: 0; /* 去除可能的默认缩进 */
|
||||
}
|
||||
.el-dialog__body .label {
|
||||
text-align: right;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
103
power_leasing/src/views/account/orderDetail.vue
Normal file
103
power_leasing/src/views/account/orderDetail.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="order-detail-page">
|
||||
<h2 class="title">订单详情</h2>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else>
|
||||
<el-card class="section">
|
||||
<div class="row"><span class="label">订单ID:</span><span class="value mono">{{ order.id || '—' }}</span></div>
|
||||
<div class="row"><span class="label">订单号:</span><span class="value mono">{{ order.orderNumber || '—' }}</span></div>
|
||||
<div class="row"><span class="label">状态:</span><span class="value">{{ order.status }}</span></div>
|
||||
<div class="row"><span class="label">金额(USDT):</span><span class="value strong">{{ order.totalPrice }}</span></div>
|
||||
<div class="row"><span class="label">创建时间:</span><span class="value">{{ formatDateTime(order.createTime) }}</span></div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="section" style="margin-top:12px;">
|
||||
<div class="sub-title">机器列表</div>
|
||||
<el-table :data="items" border size="small" style="width:100%"
|
||||
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
|
||||
<el-table-column prop="productMachineId" label="机器ID" min-width="120" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" />
|
||||
<el-table-column prop="payCoin" label="币种" min-width="100" />
|
||||
<el-table-column prop="leaseTime" label="租赁天数" min-width="100" />
|
||||
<el-table-column prop="price" label="单价(USDT)" min-width="120" />
|
||||
<el-table-column prop="address" label="收款地址" min-width="240" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div class="actions">
|
||||
<el-button @click="$router.back()">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOrdersByIds } from '../../api/order'
|
||||
|
||||
export default {
|
||||
name: 'AccountOrderDetail',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
order: {},
|
||||
items: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
const id = this.$route.params.id
|
||||
if (!id) {
|
||||
this.$message({
|
||||
message: '订单ID缺失',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.loading = true
|
||||
const res = await getOrdersByIds({ orderId: id })
|
||||
const payload = (res && res.data) != null ? res.data : res
|
||||
let one = {}
|
||||
if (Array.isArray(payload) && payload.length) one = payload[0]
|
||||
else if (payload && typeof payload === 'object') one = payload
|
||||
else if (Array.isArray(res && res.rows) && res.rows.length) one = res.rows[0]
|
||||
this.order = one || {}
|
||||
this.items = Array.isArray(one && one.orderItemDtoList) ? one.orderItemDtoList : []
|
||||
} catch (e) {
|
||||
console.log('获取订单详情失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
,
|
||||
formatDateTime(value) {
|
||||
if (!value) return '—'
|
||||
try {
|
||||
const str = String(value)
|
||||
return str.includes('T') ? str.replace('T', ' ') : str
|
||||
} catch (e) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.order-detail-page { padding: 12px; }
|
||||
.title { margin: 0 0 12px 0; font-weight: 600; color: #2c3e50; }
|
||||
.sub-title { font-weight: 600; margin-bottom: 8px; }
|
||||
.section { margin-bottom: 12px; }
|
||||
.row { display: flex; gap: 8px; line-height: 1.8; }
|
||||
.label { color: #666; }
|
||||
.value { color: #333; }
|
||||
.value.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; word-break: break-all; }
|
||||
.value.strong { font-weight: 700; color: #e74c3c; }
|
||||
.actions { margin-top: 12px; }
|
||||
</style>
|
||||
|
||||
|
||||
127
power_leasing/src/views/account/orders.vue
Normal file
127
power_leasing/src/views/account/orders.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="orders-page">
|
||||
<h2 class="title">订单列表</h2>
|
||||
<el-tabs v-model="active" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="订单进行中" name="7">
|
||||
<order-list :items="orders[7]" :show-checkout="true" :on-cancel="handleCancelOrder" empty-text="暂无进行中的订单" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="订单已完成" name="8">
|
||||
<order-list :items="orders[8]" :show-checkout="false" empty-text="暂无已完成的订单" />
|
||||
</el-tab-pane>
|
||||
<!-- <el-tab-pane label="余额不足,订单已取消" name="9">
|
||||
<order-list :items="orders[9]" :show-checkout="false" empty-text="暂无因余额不足取消的订单" />
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 订单列表页(个人中心)
|
||||
* - 使用标签页按状态展示订单
|
||||
* - 列表项可展开以显示 orderItemDtoList(机器列表)
|
||||
* - 通过 getOrdersByStatus(status) 拉取,兼容 { data: { rows: [] } } 或 { rows: [] }
|
||||
*/
|
||||
import { getOrdersByStatus, cancelOrder } from '../../api/order'
|
||||
import OrderList from './OrderList.vue'
|
||||
|
||||
// 使用外部 SFC 组件,避免 runtime-only 构建下的模板编译警告
|
||||
|
||||
export default {
|
||||
name: 'AccountOrders',
|
||||
components: { OrderList },
|
||||
data() {
|
||||
return {
|
||||
active: '7', // 默认值改为 '7'(订单进行中)
|
||||
orders: { 7: [], 8: [], 9: [] },
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 优先从 URL 参数获取状态,其次从 localStorage 获取,最后使用默认值
|
||||
const urlStatus = this.$route && this.$route.query && this.$route.query.status ? String(this.$route.query.status) : null
|
||||
const savedStatus = localStorage.getItem('orderListActiveTab')
|
||||
const initial = urlStatus || savedStatus || '7'
|
||||
|
||||
this.active = initial
|
||||
this.fetchOrders(initial)
|
||||
},
|
||||
methods: {
|
||||
async fetchCancelOrder(params) {
|
||||
const res = await cancelOrder(params)
|
||||
if (res && Number(res.code) === 200) {
|
||||
this.$message({
|
||||
message: '取消订单成功',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
this.fetchOrders(this.active)
|
||||
} else {
|
||||
this.$message({
|
||||
message: (res && res.msg) || '取消失败',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
},
|
||||
handleCancelOrder({ orderId }) {
|
||||
if (!orderId) return
|
||||
this.fetchCancelOrder({ orderId })
|
||||
},
|
||||
handleTabClick(tab) {
|
||||
const name = tab && tab.name ? String(tab.name) : this.active
|
||||
// 保存用户选择的标签页状态到 localStorage
|
||||
try {
|
||||
localStorage.setItem('orderListActiveTab', name)
|
||||
} catch (e) {
|
||||
console.warn('保存标签页状态失败:', e)
|
||||
}
|
||||
this.fetchOrders(name)
|
||||
},
|
||||
async fetchOrders(status) {
|
||||
const key = String(status)
|
||||
try {
|
||||
this.loading = true
|
||||
const res = await getOrdersByStatus({ status: Number(status) })
|
||||
const payload = (res && res.data) != null ? res.data : res
|
||||
const list = Array.isArray(payload)
|
||||
? payload
|
||||
: (Array.isArray(payload && payload.rows) ? payload.rows : [])
|
||||
this.$set(this.orders, key, list)
|
||||
} catch (e) {
|
||||
console.log(e,'获取订单失败');
|
||||
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.orders-page { padding: 12px; }
|
||||
.title { margin: 0 0 12px 0; font-weight: 600; color: #2c3e50; }
|
||||
.empty { color: #888; padding: 24px; text-align: center; }
|
||||
.order-list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.order-card { border: 1px solid #eee; border-radius: 8px; padding: 0; background: #fff; overflow: hidden; }
|
||||
.order-header { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; padding: 12px; cursor: pointer; position: relative; }
|
||||
.order-header:focus { outline: 2px solid #409eff; outline-offset: -2px; }
|
||||
.order-header.is-open { background: #fafafa; }
|
||||
.header-row { display: flex; gap: 8px; line-height: 1.8; align-items: center; }
|
||||
.chevron { position: absolute; right: 12px; top: 12px; width: 10px; height: 10px; border-right: 2px solid #666; border-bottom: 2px solid #666; transform: rotate(-45deg); transition: transform 0.2s ease; }
|
||||
.chevron.chevron-open { transform: rotate(45deg); }
|
||||
.order-details { border-top: 1px solid #eee; padding: 12px; }
|
||||
.machine-list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.machine-card { border: 1px dashed #e2e2e2; border-radius: 6px; padding: 10px; background: #fff; }
|
||||
.row { display: flex; gap: 8px; line-height: 1.8; }
|
||||
.label { color: #666; }
|
||||
.value { color: #333; }
|
||||
.value.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; word-break: break-all; }
|
||||
.value.strong { font-weight: 700; color: #e74c3c; }
|
||||
@media (max-width: 960px) {
|
||||
.order-list { grid-template-columns: 1fr; }
|
||||
.order-header { grid-template-columns: 1fr; }
|
||||
.machine-list { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
591
power_leasing/src/views/account/productDetail.vue
Normal file
591
power_leasing/src/views/account/productDetail.vue
Normal file
@@ -0,0 +1,591 @@
|
||||
<template>
|
||||
<div class="account-product-detail">
|
||||
<div class="header">
|
||||
<el-button type="text" @click="handleBack">返回</el-button>
|
||||
<h2 class="title">商品详情</h2>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息,采用工单详情的两列表单风格 -->
|
||||
<el-card shadow="never" class="detail-card">
|
||||
<el-form :model="product" label-width="90px" class="detail-form" size="small">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品ID">
|
||||
<el-input :value="product && product.id" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="店铺ID">
|
||||
<el-input :value="product && product.shopId" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="名称">
|
||||
<el-input :value="product && product.name" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="币种">
|
||||
<el-input :value="product && product.coin" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="算法">
|
||||
<el-input :value="product && product.algorithm" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="价格范围">
|
||||
<el-input :value="product && product.priceRange" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类型">
|
||||
<el-input :value="product && (product.type === 1 ? '算力套餐' : '挖矿机器')" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-input :value="product && (product.state === 1 ? '下架' : '上架')" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<!-- <el-col :span="24">
|
||||
<el-form-item label="图片">
|
||||
<div class="image-row">
|
||||
<el-image v-if="product && product.image" :src="product.image" fit="cover" class="cover" />
|
||||
<span v-else class="placeholder">暂无图片</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col> -->
|
||||
|
||||
<el-col :span="24">
|
||||
<el-form-item label="描述">
|
||||
<el-input type="textarea" :rows="3" :value="product && product.description" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 机器组合信息,模拟“处理记录”块状区域 -->
|
||||
<el-card shadow="never" class="detail-card" v-loading="updateLoading">
|
||||
<div slot="header" class="section-title">机器组合</div>
|
||||
<div v-if="machineList && machineList.length">
|
||||
<el-table :data="machineList" border stripe style="width: 100%">
|
||||
<el-table-column prop="user" label="挖矿账户" min-width="80" />
|
||||
<el-table-column prop="id" label="矿机ID" min-width="60" />
|
||||
<el-table-column prop="miner" label="机器编号" min-width="100" />
|
||||
<el-table-column label="实时算力">
|
||||
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit || '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="理论算力" min-width="140">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.theoryPower"
|
||||
size="small"
|
||||
inputmode="decimal"
|
||||
:disabled="isRowDisabled(scope.row)"
|
||||
@input="handleTheoryPowerInput(scope.$index)"
|
||||
@blur="handleTheoryPowerBlur(scope.$index)"
|
||||
:class="{ 'changed-input': isCellChanged(scope.row, 'theoryPower') }"
|
||||
style="max-width: 260px;"
|
||||
>
|
||||
<template slot="append">{{ scope.row.unit || '' }}</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="功耗(kw/h)" min-width="140">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.powerDissipation"
|
||||
size="small"
|
||||
inputmode="decimal"
|
||||
:disabled="isRowDisabled(scope.row)"
|
||||
@input="handleNumericCell(scope.$index, 'powerDissipation')"
|
||||
@blur="handlePowerDissipationBlur(scope.$index)"
|
||||
:class="{ 'changed-input': isCellChanged(scope.row, 'powerDissipation') }"
|
||||
style="max-width: 260px;"
|
||||
>
|
||||
<template slot="append">kw/h</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="型号" min-width="140">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.type"
|
||||
size="small"
|
||||
placeholder="矿机型号"
|
||||
:maxlength="20"
|
||||
:disabled="isRowDisabled(scope.row)"
|
||||
@input="handleTypeCell(scope.$index)"
|
||||
:class="{ 'changed-input': isCellChanged(scope.row, 'type') }"
|
||||
style="max-width: 180px;"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价(USDT)" min-width="140">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.price"
|
||||
size="small"
|
||||
inputmode="decimal"
|
||||
:disabled="isRowDisabled(scope.row)"
|
||||
@input="handleNumericCell(scope.$index, 'price')"
|
||||
@blur="handlePriceBlur(scope.$index)"
|
||||
:class="{ 'changed-input': isCellChanged(scope.row, 'price') }"
|
||||
style="max-width: 260px;"
|
||||
>
|
||||
<template slot="append">USDT</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上下架" min-width="140">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.state"
|
||||
:active-value="0"
|
||||
:inactive-value="1"
|
||||
active-text="上架"
|
||||
inactive-text="下架"
|
||||
:disabled="isRowDisabled(scope.row)"
|
||||
@change="handleStateChange(scope.$index)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="售出状态" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.saleState === 0 ? 'info' : (scope.row.saleState === 1 ? 'danger' : 'warning')">
|
||||
{{ scope.row.saleState === 0 ? '未售出' : (scope.row.saleState === 1 ? '已售出' : '售出中') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-button type="text" size="small" style="color:#f56c6c" :disabled="isRowDisabled(scope.row)" @click="handleDeleteMachine(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else class="empty-text">暂无组合数据</div>
|
||||
</el-card>
|
||||
<div class="actions" v-if="machineList && machineList.length">
|
||||
<el-button type="primary" @click="handleOpenConfirm">提交修改机器</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 提交确认弹窗 -->
|
||||
<el-dialog
|
||||
title="确认提交修改"
|
||||
:visible.sync="confirmVisible"
|
||||
width="520px"
|
||||
>
|
||||
<div>
|
||||
<p>请仔细确认已选择机器机器组合里的机器价格及相关参数定义。</p>
|
||||
<p>机器修改上架后,一经售出,在机器出售期间不能修改价格及机器参数。</p>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="confirmVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitMachines">确认提交修改</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getMachineInfoById } from '../../api/products'
|
||||
import { getMachineListForUpdate,updateMachine,deleteMachine } from '../../api/machine'
|
||||
|
||||
export default {
|
||||
name: 'AccountProductDetail',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
product: null,
|
||||
ranges: [],
|
||||
machineList: [],
|
||||
productId:null,
|
||||
confirmVisible: false,
|
||||
// 机器“上下架状态”快照,用于失败回滚:key 为机器 id,value 为提交前的 state
|
||||
stateSnapshot: {},
|
||||
// 可编辑字段快照(用于变更高亮)
|
||||
fieldSnapshot: {},
|
||||
updateLoading:false,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
created() {
|
||||
this.productId= Number(this.$route.params.id)
|
||||
if (this.productId) {
|
||||
this.fetchDetail({ id:this.productId })
|
||||
this.fetchMachineList({ id:this.productId })
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 判断行是否不可编辑(已售出则禁用)
|
||||
* @param {Object} row - 当前行数据
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isRowDisabled(row) {
|
||||
if (!row) return false
|
||||
return Number(row.saleState) === 1
|
||||
},
|
||||
handleOpenConfirm() {
|
||||
if (!this.machineList || !this.machineList.length) {
|
||||
this.$message.warning('没有可提交的数据')
|
||||
return
|
||||
}
|
||||
this.confirmVisible = true
|
||||
},
|
||||
async fetchDetail(params) {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await getMachineInfoById(params)
|
||||
const data = res?.data || {}
|
||||
this.product = data
|
||||
this.ranges = Array.isArray(data.productMachineRangeList) ? data.productMachineRangeList : []
|
||||
} catch (e) {
|
||||
console.error('获取商品详情失败', e)
|
||||
console.log('获取商品详情失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async fetchMachineList(params) {
|
||||
const res = await getMachineListForUpdate(params)
|
||||
|
||||
|
||||
if (res && res.code === 200) {
|
||||
this.machineList =res.rows
|
||||
this.refreshStateSnapshot()
|
||||
this.refreshFieldSnapshot()
|
||||
}
|
||||
|
||||
|
||||
|
||||
},
|
||||
/**
|
||||
* 刷新状态快照:以当前列表为准记录每条机器的 state
|
||||
* @returns {void}
|
||||
*/
|
||||
refreshStateSnapshot() {
|
||||
const snapshot = {}
|
||||
const list = Array.isArray(this.machineList) ? this.machineList : []
|
||||
for (let i = 0; i < list.length; i += 1) {
|
||||
const row = list[i]
|
||||
if (row && typeof row.id !== 'undefined') {
|
||||
snapshot[row.id] = row.state
|
||||
}
|
||||
}
|
||||
this.stateSnapshot = snapshot
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新可编辑字段快照,用于“变更高亮”对比
|
||||
* @returns {void}
|
||||
*/
|
||||
refreshFieldSnapshot() {
|
||||
const snapshot = {}
|
||||
const list = Array.isArray(this.machineList) ? this.machineList : []
|
||||
for (let i = 0; i < list.length; i += 1) {
|
||||
const row = list[i]
|
||||
if (!row || typeof row.id === 'undefined') continue
|
||||
snapshot[row.id] = {
|
||||
theoryPower: String(row.theoryPower ?? ''),
|
||||
powerDissipation: String(row.powerDissipation ?? ''),
|
||||
type: String(row.type ?? ''),
|
||||
price: String(row.price ?? ''),
|
||||
}
|
||||
}
|
||||
this.fieldSnapshot = snapshot
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断单元格是否被修改(与快照对比)
|
||||
* @param {Object} row
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCellChanged(row, key) {
|
||||
if (!row || typeof row.id === 'undefined') return false
|
||||
const snap = this.fieldSnapshot[row.id] || {}
|
||||
const current = String(row[key] ?? '')
|
||||
const original = String(snap[key] ?? '')
|
||||
return current !== original
|
||||
},
|
||||
|
||||
/**
|
||||
* 回滚上下架状态:当提交失败时将每行的 state 恢复为提交前
|
||||
* @returns {void}
|
||||
*/
|
||||
restoreStateSnapshot() {
|
||||
if (!this.machineList || !this.machineList.length) return
|
||||
for (let i = 0; i < this.machineList.length; i += 1) {
|
||||
const currentRow = this.machineList[i]
|
||||
if (!currentRow || typeof currentRow.id === 'undefined') continue
|
||||
const prevState = this.stateSnapshot[currentRow.id]
|
||||
if (typeof prevState !== 'undefined') {
|
||||
this.$set(this.machineList[i], 'state', prevState)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交机器修改;失败或异常时回滚上下架状态
|
||||
* @param {Array} params - 要更新的机器列表 payload
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateMachineList(params) {
|
||||
this.updateLoading = true
|
||||
try {
|
||||
const res = await updateMachine(params)
|
||||
if (res && res.code === 200) {
|
||||
this.$message.success('更新成功')
|
||||
// 成功后刷新列表(并在列表刷新后更新快照)
|
||||
this.fetchMachineList({ id:this.productId })
|
||||
} else {
|
||||
|
||||
// 失败回滚上下架状态
|
||||
this.restoreStateSnapshot()
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// 异常回滚上下架状态
|
||||
this.restoreStateSnapshot()
|
||||
}
|
||||
|
||||
this.updateLoading = false
|
||||
},
|
||||
async deleteMachine(params) {
|
||||
const res = await deleteMachine(params)
|
||||
if (res && res.code === 200) {
|
||||
this.$message.success('删除成功')
|
||||
this.fetchMachineList({ id:this.productId })
|
||||
}
|
||||
},
|
||||
handleTheoryPowerInput(index) {
|
||||
const rowItem = this.machineList && this.machineList[index]
|
||||
if (!rowItem || this.isRowDisabled(rowItem)) return
|
||||
// 输入阶段限制:整数最多6位,小数最多4位;允许结尾小数点
|
||||
let v = String(this.machineList[index].theoryPower ?? '')
|
||||
v = v.replace(/[^0-9.]/g, '')
|
||||
const firstDot = v.indexOf('.')
|
||||
if (firstDot !== -1) {
|
||||
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
|
||||
}
|
||||
const endsWithDot = v.endsWith('.')
|
||||
const parts = v.split('.')
|
||||
let intPart = parts[0] || ''
|
||||
let decPart = parts[1] || ''
|
||||
if (intPart.length > 6) { intPart = intPart.slice(0, 6) }
|
||||
if (decPart) { decPart = decPart.slice(0, 4) }
|
||||
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
|
||||
this.$set(this.machineList, index, { ...this.machineList[index], theoryPower: v })
|
||||
},
|
||||
handleNumericCell(index, key) {
|
||||
const rowItem = this.machineList && this.machineList[index]
|
||||
if (!rowItem || this.isRowDisabled(rowItem)) return
|
||||
// 输入阶段限制:
|
||||
// - 功耗:6 位整数 + 4 位小数
|
||||
// - 价格:12 位整数 + 2 位小数
|
||||
// - 其他:保持原逻辑(6 位小数)
|
||||
let v = String(this.machineList[index][key] ?? '')
|
||||
v = v.replace(/[^0-9.]/g, '')
|
||||
const firstDot = v.indexOf('.')
|
||||
if (firstDot !== -1) {
|
||||
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
|
||||
}
|
||||
if (key === 'powerDissipation') {
|
||||
const endsWithDot = v.endsWith('.')
|
||||
const parts = v.split('.')
|
||||
let intPart = parts[0] || ''
|
||||
let decPart = parts[1] || ''
|
||||
if (intPart.length > 6) { intPart = intPart.slice(0, 6) }
|
||||
if (decPart) { decPart = decPart.slice(0, 4) }
|
||||
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
|
||||
} else if (key === 'price') {
|
||||
const endsWithDot = v.endsWith('.')
|
||||
const parts = v.split('.')
|
||||
let intPart = parts[0] || ''
|
||||
let decPart = parts[1] || ''
|
||||
if (intPart.length > 12) { intPart = intPart.slice(0, 12) }
|
||||
if (decPart) { decPart = decPart.slice(0, 2) }
|
||||
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
|
||||
} else {
|
||||
if (firstDot !== -1) {
|
||||
const [i, d] = v.split('.')
|
||||
v = i + '.' + (d ? d.slice(0, 6) : '')
|
||||
}
|
||||
}
|
||||
const row = { ...this.machineList[index], [key]: v }
|
||||
this.$set(this.machineList, index, row)
|
||||
},
|
||||
handlePriceBlur(index) {
|
||||
const raw = String(this.machineList[index].price ?? '')
|
||||
const pattern = /^\d{1,12}(\.\d{1,2})?$/
|
||||
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
|
||||
this.$message.warning('单价必须大于0,整数最多12位,小数最多2位')
|
||||
const row = { ...this.machineList[index], price: '' }
|
||||
this.$set(this.machineList, index, row)
|
||||
}
|
||||
},
|
||||
handleTheoryPowerBlur(index) {
|
||||
const raw = String(this.machineList[index].theoryPower ?? '')
|
||||
const pattern = /^\d{1,6}(\.\d{1,4})?$/
|
||||
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
|
||||
this.$message.warning('理论算力必须大于0')
|
||||
const row = { ...this.machineList[index], theoryPower: '' }
|
||||
this.$set(this.machineList, index, row)
|
||||
}
|
||||
},
|
||||
handlePowerDissipationBlur(index) {
|
||||
const raw = String(this.machineList[index].powerDissipation ?? '')
|
||||
const pattern = /^\d{1,6}(\.\d{1,4})?$/
|
||||
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
|
||||
this.$message.warning('功耗必须大于0')
|
||||
const row = { ...this.machineList[index], powerDissipation: '' }
|
||||
this.$set(this.machineList, index, row)
|
||||
}
|
||||
},
|
||||
handleTypeCell(index) {
|
||||
const rowItem = this.machineList && this.machineList[index]
|
||||
if (!rowItem || this.isRowDisabled(rowItem)) return
|
||||
const row = { ...this.machineList[index], type: this.machineList[index].type }
|
||||
this.$set(this.machineList, index, row)
|
||||
},
|
||||
handleStateChange(index) {
|
||||
const rowItem = this.machineList && this.machineList[index]
|
||||
if (!rowItem || this.isRowDisabled(rowItem)) return
|
||||
const row = { ...this.machineList[index], state: this.machineList[index].state }
|
||||
this.$set(this.machineList, index, row)
|
||||
},
|
||||
async handleDeleteMachine(row) {
|
||||
if (!row || !row.id) return
|
||||
if (this.isRowDisabled(row)) {
|
||||
this.$message.warning('该矿机已售出,无法删除')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$confirm('确定删除该矿机吗?删除后不可恢复', '提示', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await deleteMachine({ id: row.id })
|
||||
if (res && res.code === 200) {
|
||||
this.$message.success('删除成功')
|
||||
this.fetchMachineList({ id: this.productId })
|
||||
}
|
||||
} catch (e) {
|
||||
// 用户取消
|
||||
}
|
||||
},
|
||||
async handleSubmitMachines() {
|
||||
if (!this.machineList || !this.machineList.length) {
|
||||
this.$message.warning('没有可提交的数据')
|
||||
return
|
||||
}
|
||||
try {
|
||||
// 提交前强校验:理论算力、功耗、单价必须填写且格式正确(全部行都校验)
|
||||
const powerPattern = /^\d{1,6}(\.\d{1,4})?$/
|
||||
const pricePattern = /^\d{1,12}(\.\d{1,2})?$/
|
||||
const isOnlySpaces = (v) => typeof v === 'string' && v.trim().length === 0 && v.length > 0
|
||||
for (let i = 0; i < this.machineList.length; i += 1) {
|
||||
const row = this.machineList[i]
|
||||
const rowLabel = row && (row.miner || row.id || i + 1)
|
||||
const theoryRaw = String(row.theoryPower ?? '')
|
||||
const priceRaw = String(row.price ?? '')
|
||||
const typeRaw = String(row.type ?? '')
|
||||
const dissRaw = String(row.powerDissipation ?? '')
|
||||
|
||||
if (!theoryRaw || Number(theoryRaw) <= 0 || !powerPattern.test(theoryRaw)) {
|
||||
this.$message.warning(`第${i + 1}行(机器:${rowLabel}) 理论算力必须大于0,整数最多6位,小数最多4位`)
|
||||
return
|
||||
}
|
||||
if (!dissRaw || Number(dissRaw) <= 0 || !powerPattern.test(dissRaw)) {
|
||||
this.$message.warning(`第${i + 1}行(机器:${rowLabel}) 功耗必须大于0,整数最多6位,小数最多4位`)
|
||||
return
|
||||
}
|
||||
if (!priceRaw || Number(priceRaw) <= 0 || !pricePattern.test(priceRaw)) {
|
||||
this.$message.warning(`第${i + 1}行(机器:${rowLabel}) 单价必须大于0,整数最多12位,小数最多2位`)
|
||||
return
|
||||
}
|
||||
// 型号允许为空,但如果填写则不能全空格
|
||||
if (typeRaw && isOnlySpaces(typeRaw)) {
|
||||
this.$message.warning(`第${i + 1}行(机器:${rowLabel}) 型号不能全是空格`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const payload = this.machineList.map(m => ({
|
||||
id: m.id,
|
||||
powerDissipation: Number(m.powerDissipation ?? 0),
|
||||
price: Number(m.price ?? 0),
|
||||
state: Number(m.state ?? 0),
|
||||
theoryPower: Number(m.theoryPower ?? 0),
|
||||
type: m.type || '',
|
||||
unit: m.unit || ''
|
||||
}))
|
||||
|
||||
// 关闭外层确认弹窗,直接提交
|
||||
this.confirmVisible = false
|
||||
|
||||
console.log(payload, 'payload');
|
||||
await this.updateMachineList(payload)
|
||||
} catch (e) {
|
||||
// 异常处理
|
||||
}
|
||||
},
|
||||
handleBack() {
|
||||
this.$router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.account-product-detail { padding: 8px; }
|
||||
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||
.title { margin: 0; font-size: 18px; font-weight: 600; }
|
||||
.detail-card { margin-bottom: 12px; }
|
||||
.detail-form { padding: 4px 8px; }
|
||||
.image-row { display: flex; align-items: center; min-height: 120px; }
|
||||
.cover { width: 200px; height: 120px; object-fit: cover; border-radius: 4px; background: #f5f5f5; border: 1px solid #eee; }
|
||||
.placeholder { color: #999; }
|
||||
.section-title { font-weight: 600; }
|
||||
.ranges-wrapper { display: grid; gap: 12px; }
|
||||
.range-block { border: 1px solid #f0f0f0; background: #fcfcfc; border-radius: 6px; padding: 10px; }
|
||||
.item { color: #444; line-height: 24px; }
|
||||
.machines-box { margin-top: 8px; border-top: 1px dashed #e5e5e5; padding-top: 8px; }
|
||||
.machine-row { display: flex; flex-wrap: wrap; gap: 8px; color: #555; line-height: 22px; }
|
||||
.split { width: 8px; }
|
||||
.empty-text { color: #909399; text-align: center; padding: 12px 0; }
|
||||
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.el-input-group__append, .el-input-group__prepend{
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
|
||||
/* 变化高亮:为输入框外层添加红色边框,视觉醒目但不改变布局 */
|
||||
.changed-input .el-input__inner,
|
||||
.changed-input input.el-input__inner {
|
||||
border-color: #f56c6c !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
668
power_leasing/src/views/account/productMachineAdd.vue
Normal file
668
power_leasing/src/views/account/productMachineAdd.vue
Normal file
@@ -0,0 +1,668 @@
|
||||
<template>
|
||||
<div class="product-machine-add">
|
||||
<div class="header">
|
||||
<el-button type="text" @click="handleBack">返回</el-button>
|
||||
<h2 class="title">添加出售机器</h2>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="form-card">
|
||||
<el-form ref="machineForm" :model="form" :rules="rules" label-width="160px" size="small">
|
||||
<el-form-item label="商品名称">
|
||||
<el-input v-model="form.productName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="功耗" prop="powerDissipation">
|
||||
<el-input
|
||||
v-model="form.powerDissipation"
|
||||
placeholder="示例:0.01"
|
||||
inputmode="decimal"
|
||||
@input="handleNumeric('powerDissipation')"
|
||||
>
|
||||
<template slot="append">kw/h</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="机器成本价格" prop="cost">
|
||||
<el-input
|
||||
v-model="form.cost"
|
||||
placeholder="请输入成本(USDT)"
|
||||
inputmode="decimal"
|
||||
@input="handleNumeric('cost')"
|
||||
>
|
||||
<template slot="append">USDT</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<el-form-item label="矿机型号">
|
||||
<el-input v-model="form.type" placeholder="示例:龍珠" :maxlength="20" @input="handleTypeInput" />
|
||||
</el-form-item>
|
||||
<el-form-item label="理论算力" prop="theoryPower">
|
||||
<el-input
|
||||
v-model="form.theoryPower"
|
||||
placeholder="请输入单机理论算力"
|
||||
inputmode="decimal"
|
||||
@input="handleNumeric('theoryPower')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="算力单位" prop="unit">
|
||||
<el-select v-model="form.unit" placeholder="请选择算力单位">
|
||||
<el-option label="MH/S" value="MH/S" />
|
||||
<el-option label="GH/S" value="GH/S" />
|
||||
<el-option label="TH/S" value="TH/S" />
|
||||
<el-option label="PH/S" value="PH/S" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择挖矿账户">
|
||||
<el-select v-model="selectedMiner" filterable clearable placeholder="请选择挖矿账户" @change="handleMinerChange" :loading="minersLoading">
|
||||
<el-option v-for="m in miners" :key="m.user + '_' + m.coin" :label="m.user + '(' + m.coin + ')'" :value="m.user + '|' + m.coin" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择机器(可多选)">
|
||||
<el-select v-model="selectedMachines" multiple filterable collapse-tags placeholder="请选择机器" :loading="machinesLoading" :disabled="!selectedMiner">
|
||||
<el-option v-for="m in machineOptions" :key="m.user + '_' + m.miner" :label="m.miner + '(' + m.user + ')'" :value="m.miner" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 已选择机器列表 -->
|
||||
<el-card shadow="never" class="form-card" v-if="selectedMachineRows.length">
|
||||
<div slot="header" class="section-title">已选择机器</div>
|
||||
<el-table :data="selectedMachineRows" border stripe style="width: 100%">
|
||||
<el-table-column prop="user" label="挖矿账户" min-width="160" />
|
||||
<el-table-column prop="miner" label="机器编号" min-width="160" />
|
||||
<el-table-column label="价格(USDT)" min-width="220">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.price"
|
||||
placeholder="价格"
|
||||
inputmode="decimal"
|
||||
@input="handleRowPriceInput(scope.$index)"
|
||||
@blur="handleRowPriceBlur(scope.$index)"
|
||||
style="width: 70%;"
|
||||
>
|
||||
<template slot="append">USDT</template>
|
||||
</el-input>
|
||||
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
<el-table-column label="矿机型号" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.type"
|
||||
placeholder="矿机型号"
|
||||
@input="handleRowTypeInput(scope.$index)"
|
||||
@blur="handleRowTypeBlur(scope.$index)"
|
||||
:maxlength="20"
|
||||
style="width: 70%;"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上下架状态" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
:type="scope.row.state === 0 ? 'success' : 'info'"
|
||||
size="mini"
|
||||
@click="handleToggleState(scope.$index)"
|
||||
>
|
||||
{{ scope.row.state === 0 ? '上架' : '下架' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div class="actions">
|
||||
<el-button @click="handleBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">确认添加</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 上架确认弹窗 -->
|
||||
<el-dialog
|
||||
title="请确认上架信息"
|
||||
:visible.sync="confirmVisible"
|
||||
width="400px"
|
||||
>
|
||||
<div>
|
||||
<p>请仔细确认已选择机器列表、价格及相关参数定义。</p>
|
||||
<p style="text-align: left;">机器上架后,一经售出,在机器出售期间不能修改价格及机器参数。</p>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="confirmVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="doSubmit">确认上架已选择机器</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserMinersList, getUserMachineList, addSingleOrBatchMachine } from '../../api/machine'
|
||||
|
||||
export default {
|
||||
name: 'AccountProductMachineAdd',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
productId: Number(this.$route.query.productId) || null,
|
||||
coin: this.$route.query.coin || '',
|
||||
productName: this.$route.query.name || '',
|
||||
powerDissipation: null,
|
||||
theoryPower: null,
|
||||
type: '',
|
||||
unit: 'TH/S',
|
||||
cost: ''
|
||||
},
|
||||
confirmVisible: false,
|
||||
rules: {
|
||||
productName: [ { required: true, message: '商品名称不能为空', trigger: 'change' } ],
|
||||
coin: [ { required: true, message: '币种不能为空', trigger: 'change' } ],
|
||||
powerDissipation: [
|
||||
{ required: true, message: '功耗不能为空', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const str = String(value || '')
|
||||
if (!str) { callback(new Error('功耗不能为空')); return }
|
||||
const pattern = /^\d{1,6}(\.\d{1,4})?$/
|
||||
if (!pattern.test(str)) { callback(new Error('功耗整数最多6位,小数最多4位')); return }
|
||||
if (Number(str) <= 0) { callback(new Error('功耗必须大于0')); return }
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
theoryPower: [
|
||||
{ required: true, message: '理论算力不能为空', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const str = String(value || '')
|
||||
if (!str) { callback(new Error('理论算力不能为空')); return }
|
||||
const pattern = /^\d{1,6}(\.\d{1,4})?$/
|
||||
if (!pattern.test(str)) { callback(new Error('理论算力整数最多6位,小数最多4位')); return }
|
||||
if (Number(str) <= 0) { callback(new Error('理论算力必须大于0')); return }
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
unit: [ { required: true, message: '请选择算力单位', trigger: 'change' } ],
|
||||
cost: [
|
||||
{ required: true, message: '请填写机器成本(USDT)', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const str = String(value || '')
|
||||
if (!str) {
|
||||
callback(new Error('请填写机器成本(USDT)'))
|
||||
return
|
||||
}
|
||||
// 整数最多12位,小数最多2位
|
||||
const pattern = /^\d{1,12}(\.\d{1,2})?$/
|
||||
if (!pattern.test(str)) {
|
||||
callback(new Error('成本整数最多12位,小数最多2位'))
|
||||
return
|
||||
}
|
||||
if (Number(str) <= 0) {
|
||||
callback(new Error('成本必须大于 0'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
},
|
||||
miners: [
|
||||
// {
|
||||
// "user": "lx_888",
|
||||
// "miner": null,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx999",
|
||||
// "miner": null,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx88",
|
||||
// "miner": null,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx6666",
|
||||
// "miner": null,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx_999",
|
||||
// "miner": null,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "Lx_6966",
|
||||
// "miner": null,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "LX_666",
|
||||
// "miner": null,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
],
|
||||
minersLoading: false,
|
||||
selectedMiner: '', // 格式 user|coin
|
||||
machineOptions: [
|
||||
// {
|
||||
// "user": "lx_888",
|
||||
// "miner": `iusfhufhu`,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx999",
|
||||
// "miner": `iusfhufhu2`,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx88",
|
||||
// "miner": `iusfhufhu3`,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx6666",
|
||||
// "miner": `iusfhufhu4`,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "lx_999",
|
||||
// "miner": `iusfhufhu5`,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "Lx_6966",
|
||||
// "miner": `iusfhufhu6`,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
// {
|
||||
// "user": "LX_666",
|
||||
// "miner": `iusfhufhu7`,
|
||||
// "coin": "nexa"
|
||||
// },
|
||||
],
|
||||
machinesLoading: false,
|
||||
selectedMachines: [],
|
||||
selectedMachineRows: [],
|
||||
saving: false,
|
||||
lastCostBaseline: 0,
|
||||
lastTypeBaseline: '',
|
||||
params:{
|
||||
cost:353400,
|
||||
powerDissipation:0.01,
|
||||
theoryPower:1000,
|
||||
type:"",
|
||||
unit:"TH/S",
|
||||
productId:1,
|
||||
productMachineURDVos:[
|
||||
{
|
||||
"user":"lx_888",
|
||||
"miner":"iusfhufhu",
|
||||
"price":353400,
|
||||
"type":"",
|
||||
"state":0
|
||||
},
|
||||
{
|
||||
"user":"lx_888",
|
||||
"miner":"iusfhufhu2",
|
||||
"price":353400,
|
||||
"type":"",
|
||||
"state":0
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchMiners()
|
||||
this.lastTypeBaseline = this.form.type
|
||||
},
|
||||
methods: {
|
||||
handleBack() {
|
||||
this.$router.back()
|
||||
},
|
||||
handleNumeric(key) {
|
||||
// 仅允许数字和一个小数点
|
||||
let v = String(this.form[key] ?? '')
|
||||
// 清理非法字符
|
||||
v = v.replace(/[^0-9.]/g, '')
|
||||
// 保留第一个小数点
|
||||
const firstDot = v.indexOf('.')
|
||||
if (firstDot !== -1) {
|
||||
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
|
||||
}
|
||||
const endsWithDot = v.endsWith('.')
|
||||
if (key === 'cost') {
|
||||
// 成本:整数最多12位,小数最多2位
|
||||
const parts = v.split('.')
|
||||
let intPart = parts[0] || ''
|
||||
let decPart = parts[1] || ''
|
||||
if (intPart.length > 12) {
|
||||
intPart = intPart.slice(0, 12)
|
||||
}
|
||||
if (decPart) {
|
||||
decPart = decPart.slice(0, 2)
|
||||
}
|
||||
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
|
||||
} else if (key === 'powerDissipation' || key === 'theoryPower') {
|
||||
// 功耗/理论算力:整数最多6位,小数最多4位
|
||||
const parts = v.split('.')
|
||||
let intPart = parts[0] || ''
|
||||
let decPart = parts[1] || ''
|
||||
if (intPart.length > 6) {
|
||||
intPart = intPart.slice(0, 6)
|
||||
}
|
||||
if (decPart) {
|
||||
decPart = decPart.slice(0, 4)
|
||||
}
|
||||
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
|
||||
} else {
|
||||
// 其他:最多6位小数(保持原有逻辑)
|
||||
if (firstDot !== -1) {
|
||||
const [intPart, decPart] = v.split('.')
|
||||
v = intPart + '.' + (decPart ? decPart.slice(0, 6) : '')
|
||||
}
|
||||
}
|
||||
this.form[key] = v
|
||||
if (key === 'cost') {
|
||||
this.syncCostToRows()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 顶部矿机型号输入:限制20字符
|
||||
*/
|
||||
handleTypeInput() {
|
||||
if (typeof this.form.type === 'string' && this.form.type.length > 20) {
|
||||
this.form.type = this.form.type.slice(0, 20)
|
||||
}
|
||||
},
|
||||
syncCostToRows() {
|
||||
const newCost = Number(this.form.cost)
|
||||
if (!Number.isFinite(newCost)) {
|
||||
return
|
||||
}
|
||||
const oldBaseline = this.lastCostBaseline
|
||||
this.selectedMachineRows = this.selectedMachineRows.map(row => {
|
||||
const priceNum = Number(row.price)
|
||||
if (!Number.isFinite(priceNum) || priceNum === oldBaseline) {
|
||||
return { ...row, price: newCost }
|
||||
}
|
||||
return row
|
||||
})
|
||||
this.lastCostBaseline = newCost
|
||||
},
|
||||
updateMachineType() {
|
||||
// 当外层矿机型号变动时,同步更新机器列表中的型号
|
||||
// 但如果用户手动改过某行型号,则不覆盖
|
||||
this.selectedMachineRows = this.selectedMachineRows.map(row => {
|
||||
// 如果该行型号为空或等于旧型号,则更新为新型号
|
||||
if (!row.type || row.type === this.lastTypeBaseline) {
|
||||
return { ...row, type: this.form.type }
|
||||
}
|
||||
return row
|
||||
})
|
||||
this.lastTypeBaseline = this.form.type
|
||||
},
|
||||
updateSelectedMachineRows() {
|
||||
// 依据 selectedMachines 与 machineOptions 同步生成行数据
|
||||
const map = new Map()
|
||||
this.machineOptions.forEach(m => {
|
||||
map.set(m.miner, m)
|
||||
})
|
||||
const nextRows = []
|
||||
this.selectedMachines.forEach(minerId => {
|
||||
const m = map.get(minerId)
|
||||
if (m) {
|
||||
// 若已存在,沿用已编辑的价格、型号和状态
|
||||
const existed = this.selectedMachineRows.find(r => r.miner === minerId)
|
||||
nextRows.push({
|
||||
user: m.user,
|
||||
coin: m.coin,
|
||||
miner: m.miner,
|
||||
price: existed ? existed.price : this.form.cost,
|
||||
type: existed ? existed.type : this.form.type,
|
||||
state: existed ? existed.state : 0 // 默认上架
|
||||
})
|
||||
}
|
||||
})
|
||||
this.selectedMachineRows = nextRows
|
||||
},
|
||||
handleRowPriceInput(index) {
|
||||
// 价格输入:整数最多12位,小数最多2位;允许尾随小数点
|
||||
let v = String(this.selectedMachineRows[index].price ?? '')
|
||||
v = v.replace(/[^0-9.]/g, '')
|
||||
const firstDot = v.indexOf('.')
|
||||
if (firstDot !== -1) {
|
||||
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
|
||||
}
|
||||
const endsWithDot = v.endsWith('.')
|
||||
const parts = v.split('.')
|
||||
let intPart = parts[0] || ''
|
||||
let decPart = parts[1] || ''
|
||||
if (intPart.length > 12) { intPart = intPart.slice(0, 12) }
|
||||
if (decPart) { decPart = decPart.slice(0, 2) }
|
||||
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
|
||||
this.$set(this.selectedMachineRows[index], 'price', v)
|
||||
},
|
||||
handleRowPriceBlur(index) {
|
||||
const raw = String(this.selectedMachineRows[index].price ?? '')
|
||||
const pattern = /^\d{1,12}(\.\d{1,2})?$/
|
||||
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
|
||||
this.$message.warning('价格必须大于0,整数最多12位,小数最多2位')
|
||||
this.$set(this.selectedMachineRows[index], 'price', '')
|
||||
}
|
||||
},
|
||||
handleRowTypeInput(index) {
|
||||
// 处理矿机型号输入
|
||||
const raw = String(this.selectedMachineRows[index].type || '')
|
||||
const v = raw.length > 20 ? raw.slice(0, 20) : raw
|
||||
this.$set(this.selectedMachineRows[index], 'type', v)
|
||||
},
|
||||
handleRowTypeBlur(index) {
|
||||
const raw = this.selectedMachineRows[index].type
|
||||
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
|
||||
if (isOnlySpaces(raw)) {
|
||||
this.$message.warning('矿机型号不能全是空格')
|
||||
this.$set(this.selectedMachineRows[index], 'type', '')
|
||||
}
|
||||
},
|
||||
handleToggleState(index) {
|
||||
// 切换上下架状态:0上架,1下架
|
||||
const currentState = this.selectedMachineRows[index].state
|
||||
this.$set(this.selectedMachineRows[index], 'state', currentState === 0 ? 1 : 0)
|
||||
},
|
||||
async fetchMiners() {
|
||||
this.minersLoading = true
|
||||
try {
|
||||
// 按商品币种筛选挖矿账户
|
||||
const res = await getUserMinersList({ coin: this.form.coin || "" })
|
||||
const data = res?.data
|
||||
let list = []
|
||||
if (Array.isArray(data)) {
|
||||
list = data
|
||||
} else if (data && typeof data === 'object') {
|
||||
// 现在的结构是 { coin: [ { user, coin }, ... ], coin2: [...] }
|
||||
Object.keys(data).forEach(coinKey => {
|
||||
const arr = Array.isArray(data[coinKey]) ? data[coinKey] : []
|
||||
arr.forEach(item => {
|
||||
if (item && item.user && item.coin) {
|
||||
list.push({ user: item.user, coin: item.coin, miner: item.miner || null })
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if (data && data.additionalProperties1) {
|
||||
list = [data.additionalProperties1]
|
||||
}
|
||||
|
||||
// 如页面带了 product coin,则仅展示该币种的账户
|
||||
if (this.form.coin) {
|
||||
list = list.filter(i => i.coin === this.form.coin)
|
||||
}
|
||||
this.miners = list
|
||||
} catch (e) {
|
||||
console.error('获取挖矿账户失败', e)
|
||||
} finally {
|
||||
this.minersLoading = false
|
||||
}
|
||||
},
|
||||
async handleMinerChange(val) {
|
||||
this.selectedMachines = []
|
||||
if (!val) {
|
||||
this.machineOptions = []
|
||||
return
|
||||
}
|
||||
const [user, coin] = val.split('|')
|
||||
this.machinesLoading = true
|
||||
try {
|
||||
// 按照API文档要求,传递 userMinerVo 对象
|
||||
const userMinerVo = {
|
||||
coin: coin,
|
||||
user: user
|
||||
}
|
||||
const res = await getUserMachineList(userMinerVo)
|
||||
const data = res?.data || []
|
||||
this.machineOptions = Array.isArray(data) ? data : []
|
||||
|
||||
// 调试信息
|
||||
console.log('选择挖矿账户:', { user, coin })
|
||||
console.log('获取机器列表响应:', res)
|
||||
console.log('机器列表数据:', this.machineOptions)
|
||||
} catch (e) {
|
||||
console.error('获取机器列表失败', e)
|
||||
this.$message.error('获取机器列表失败,请重试')
|
||||
} finally {
|
||||
this.machinesLoading = false
|
||||
}
|
||||
},
|
||||
async handleSave() {
|
||||
// 表单校验(除矿机型号外其他必填)
|
||||
try {
|
||||
const ok = await this.$refs.machineForm.validate()
|
||||
if (!ok) {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
if (!this.form.productId) {
|
||||
this.$message.warning('缺少商品ID')
|
||||
return
|
||||
}
|
||||
if (!this.selectedMiner) {
|
||||
this.$message.warning('请先选择挖矿账户')
|
||||
return
|
||||
}
|
||||
if (!this.selectedMachines.length) {
|
||||
this.$message.warning('请至少选择一台机器')
|
||||
return
|
||||
}
|
||||
// 校验:矿机型号不可全空格(允许为空或包含空格的正常文本)
|
||||
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
|
||||
if (isOnlySpaces(this.form.type)) {
|
||||
this.$message.warning('矿机型号不能全是空格')
|
||||
return
|
||||
}
|
||||
const invalidTypeRowIndex = this.selectedMachineRows.findIndex(r => isOnlySpaces(r.type))
|
||||
if (invalidTypeRowIndex !== -1) {
|
||||
this.$message.warning('存在行的矿机型号全是空格,请修正后再试')
|
||||
return
|
||||
}
|
||||
// 校验:已选择机器的价格必须大于0
|
||||
for (let i = 0; i < this.selectedMachineRows.length; i += 1) {
|
||||
const row = this.selectedMachineRows[i]
|
||||
const priceNum = Number(row && row.price)
|
||||
if (!Number.isFinite(priceNum) || priceNum <= 0) {
|
||||
const label = (row && (row.miner || row.user)) || i + 1
|
||||
this.$message.warning(`第${i + 1}行(机器:${label}) 价格必须大于0`)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 通过所有预校验后,弹出确认框
|
||||
this.confirmVisible = true
|
||||
}
|
||||
,
|
||||
async doSubmit() {
|
||||
const [user, coin] = this.selectedMiner.split('|')
|
||||
this.saving = true
|
||||
try {
|
||||
const payload = {
|
||||
productId: this.form.productId,
|
||||
powerDissipation: this.form.powerDissipation,
|
||||
theoryPower: this.form.theoryPower,
|
||||
type: this.form.type,
|
||||
unit: this.form.unit,
|
||||
cost: this.form.cost,
|
||||
productMachineURDVos: this.selectedMachineRows.map(r => ({
|
||||
miner: r.miner,
|
||||
price: Number(r.price) || 0,
|
||||
state: r.state || 0,
|
||||
type: r.type || this.form.type,
|
||||
user: r.user
|
||||
}))
|
||||
}
|
||||
|
||||
console.log(payload,"请求参数")
|
||||
|
||||
const res = await addSingleOrBatchMachine(payload)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message.success('添加成功')
|
||||
this.confirmVisible = false
|
||||
this.$router.back()
|
||||
} else {
|
||||
this.$message.error(res?.msg || '添加失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('添加出售机器失败', e)
|
||||
console.log('添加失败')
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
,
|
||||
watch: {
|
||||
'form.cost': function() { this.syncCostToRows() },
|
||||
'form.type': function() { this.updateMachineType() },
|
||||
selectedMachines() {
|
||||
this.updateSelectedMachineRows()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-machine-add { padding: 8px; }
|
||||
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||
.title { margin: 0; font-size: 18px; font-weight: 600; }
|
||||
.form-card { margin-bottom: 12px; }
|
||||
.actions { text-align: right; }
|
||||
|
||||
/* 统一左对齐,控件宽度 50% */
|
||||
.product-machine-add :deep(.el-form-item__content) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.product-machine-add :deep(.el-input),
|
||||
.product-machine-add :deep(.el-select),
|
||||
.product-machine-add :deep(.el-textarea) {
|
||||
width: 50%;
|
||||
}
|
||||
.product-machine-add :deep(.el-input-group__append) {
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__content{
|
||||
text-align: left;
|
||||
padding-left: 18px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
443
power_leasing/src/views/account/productNew.vue
Normal file
443
power_leasing/src/views/account/productNew.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<div class="product-new">
|
||||
<el-card class="product-form-card">
|
||||
<div slot="header" class="card-header">
|
||||
<h2>新增商品</h2>
|
||||
<p class="subtitle">创建新的商品信息</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="productForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
class="product-form"
|
||||
>
|
||||
<!-- 商品名称 -->
|
||||
<el-form-item label="商品名称" prop="name">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入商品名称,如:Nexa-M2-Miner"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 商品类型 -->
|
||||
<el-form-item label="商品类型" prop="type" class="align-like-input">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio :label="0">矿机</el-radio>
|
||||
<el-radio :label="1">算力</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 矿机挖矿币种 -->
|
||||
<el-form-item label="挖矿币种" prop="coin">
|
||||
<el-select
|
||||
v-model="form.coin"
|
||||
placeholder="请选择挖矿币种"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="coin in coinOptions"
|
||||
:key="coin.value"
|
||||
:label="coin.label"
|
||||
:value="coin.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 商品描述 -->
|
||||
<el-form-item label="商品描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入商品描述"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 商品图片(非必填) -->
|
||||
<!-- <el-form-item label="商品图片" prop="image">
|
||||
<el-input
|
||||
v-model="form.image"
|
||||
placeholder="请输入商品图片路径"
|
||||
/>
|
||||
</el-form-item> -->
|
||||
|
||||
<!-- 单机算力 -->
|
||||
<!-- <el-form-item label="单机算力" prop="power">
|
||||
<el-input
|
||||
v-model.number="form.power"
|
||||
type="number"
|
||||
placeholder="请输入单机算力"
|
||||
>
|
||||
<el-select
|
||||
slot="append"
|
||||
v-model="form.unit"
|
||||
placeholder="单位"
|
||||
style="width: 110px"
|
||||
>
|
||||
<el-option label="GH/S" value="GH/S" />
|
||||
<el-option label="TH/S" value="TH/S" />
|
||||
<el-option label="PH/S" value="PH/S" />
|
||||
</el-select>
|
||||
</el-input>
|
||||
</el-form-item> -->
|
||||
|
||||
|
||||
<!-- 功耗 -->
|
||||
<!-- <el-form-item label="功耗" prop="powerDissipation">
|
||||
<el-input
|
||||
v-model.number="form.powerDissipation"
|
||||
type="number"
|
||||
placeholder="请输入功耗"
|
||||
>
|
||||
<template slot="append">kw/h</template>
|
||||
</el-input>
|
||||
</el-form-item> -->
|
||||
|
||||
<!-- 电费 -->
|
||||
<!-- <el-form-item label="电费" prop="electricityBill">
|
||||
<el-input
|
||||
v-model.number="form.electricityBill"
|
||||
type="number"
|
||||
placeholder="请输入电费"
|
||||
>
|
||||
<template slot="append">$/度</template>
|
||||
</el-input>
|
||||
</el-form-item> -->
|
||||
|
||||
<!-- 收益率 -->
|
||||
<!-- <el-form-item label="收益率" prop="incomeRate">
|
||||
<el-input
|
||||
v-model.number="form.incomeRate"
|
||||
type="number"
|
||||
placeholder="请输入收益率"
|
||||
>
|
||||
<template slot="append">%</template>
|
||||
</el-input>
|
||||
</el-form-item> -->
|
||||
|
||||
<!-- 上下架状态 -->
|
||||
<el-form-item label="商品状态" prop="state" class="align-like-input">
|
||||
<el-radio-group v-model="form.state">
|
||||
<el-radio :label="0">上架</el-radio>
|
||||
<el-radio :label="1">下架</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item class="actions-row">
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" size="medium" @click="handleSubmit" :loading="submitting">创建商品</el-button>
|
||||
<el-button size="medium" @click="handleReset">重置</el-button>
|
||||
<el-button size="medium" @click="handleCancel">取消</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { coinList } from '@/utils/coinList'
|
||||
import { createProduct } from '../../api/products'
|
||||
|
||||
export default {
|
||||
name: 'AccountProductNew',
|
||||
data() {
|
||||
const notOnlySpaces = (rule, value, callback) => {
|
||||
if (typeof value === 'string' && value.trim().length === 0) {
|
||||
callback(new Error('内容不能全是空格'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
/**
|
||||
* 检测字符串是否包含表情符号
|
||||
* @param {string} text - 待检测文本
|
||||
* @returns {boolean} 是否包含表情符号
|
||||
*/
|
||||
const containsEmoji = (text) => {
|
||||
if (typeof text !== 'string' || text.length === 0) {
|
||||
return false
|
||||
}
|
||||
// 覆盖常见 Emoji 范围、旗帜、杂项符号、交通、补充符号、ZWJ、变体选择符与组合字符等
|
||||
const emojiPattern = /[\u{1F300}-\u{1FAFF}]|[\u{1F1E6}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE0F}]|[\u{200D}]|[\u{20E3}]/u
|
||||
return emojiPattern.test(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 不允许包含表情符号的名称校验器
|
||||
* @param {object} rule - 校验规则对象
|
||||
* @param {string} value - 输入值
|
||||
* @param {Function} callback - 回调
|
||||
* @returns {void}
|
||||
*/
|
||||
const noEmoji = (rule, value, callback) => {
|
||||
if (typeof value === 'string' && containsEmoji(value)) {
|
||||
callback(new Error('商品名称不能包含表情符号'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
return {
|
||||
submitting: false,
|
||||
form: {
|
||||
name: '',
|
||||
type: 0,
|
||||
coin: '',
|
||||
description: '',//商品描述
|
||||
image: '',
|
||||
// power: null,
|
||||
// unit: 'GH/S',
|
||||
// powerDissipation: null,
|
||||
// electricityBill: null,//电费
|
||||
// incomeRate: null,//收益率
|
||||
state: 0,
|
||||
shopId: null
|
||||
},
|
||||
rules: {
|
||||
name: [
|
||||
{ required: true, message: '请输入商品名称', trigger: 'blur' },
|
||||
{ validator: notOnlySpaces, trigger: 'blur' },
|
||||
{ validator: noEmoji, trigger: 'blur' },
|
||||
{ min: 1, max: 30, message: '商品名称长度在 2 到 30 个字符', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择商品类型', trigger: 'change' }
|
||||
],
|
||||
coin: [
|
||||
{ required: true, message: '请选择挖矿币种', trigger: 'change' }
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: '请输入商品描述', trigger: 'blur' },
|
||||
{ validator: notOnlySpaces, trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '商品描述长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
image: [],
|
||||
// power: [
|
||||
// { required: true, message: '请输入单机算力', trigger: 'blur' },
|
||||
// { type: 'number', min: 0.01, message: '算力必须大于0', trigger: 'blur' }
|
||||
// ],
|
||||
// unit: [ { required: true, message: '请选择算力单位', trigger: 'change' } ],
|
||||
// powerDissipation: [
|
||||
// { required: true, message: '请输入功耗', trigger: 'blur' },
|
||||
// { type: 'number', min: 0.01, message: '功耗必须大于0', trigger: 'blur' }
|
||||
// ],
|
||||
// electricityBill: [
|
||||
// { required: true, message: '请输入电费', trigger: 'blur' },
|
||||
// { type: 'number', min: 0.01, message: '电费必须大于0', trigger: 'blur' }
|
||||
// ],
|
||||
// incomeRate: [
|
||||
// { required: true, message: '请输入收益率', trigger: 'blur' },
|
||||
// { type: 'number', min: 0.0001, max: 1, message: '收益率必须在0.0001到1之间', trigger: 'blur' }
|
||||
// ],
|
||||
state: [
|
||||
{ required: true, message: '请选择商品状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coinOptions() {
|
||||
return coinList || [
|
||||
{ value: 'nexa', label: 'NEXA' },
|
||||
{ value: 'rxd', label: 'RXD' },
|
||||
{ value: 'dgbo', label: 'DGBO' },
|
||||
{ value: 'dgbq', label: 'DGBQ' },
|
||||
{ value: 'dgbs', label: 'DGBS' },
|
||||
{ value: 'alph', label: 'ALPH' },
|
||||
{ value: 'enx', label: 'ENX' },
|
||||
{ value: 'grs', label: 'GRS' },
|
||||
{ value: 'mona', label: 'MONA' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 从路由参数获取店铺ID
|
||||
const shopId = this.$route.query.shopId
|
||||
if (shopId) {
|
||||
this.form.shopId = Number(shopId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchAddProduct(params) {
|
||||
const res = await createProduct(params)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
|
||||
this.$message({
|
||||
message: '商品创建成功',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
this.$router.push('/account/shops')
|
||||
}else {
|
||||
this.$message({
|
||||
message: res && res.msg ? res.msg : '创建失败',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
async handleSubmit() {
|
||||
try {
|
||||
// 表单验证
|
||||
const valid = await this.$refs.productForm.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有店铺ID
|
||||
if (!this.form.shopId) {
|
||||
this.$message({
|
||||
message: '缺少店铺ID,请从我的店铺页面进入',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.submitting = true
|
||||
|
||||
this.fetchAddProduct(this.form)
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建商品失败:', error)
|
||||
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
handleReset() {
|
||||
this.$refs.productForm.resetFields()
|
||||
// 保持店铺ID
|
||||
const shopId = this.$route.query.shopId
|
||||
if (shopId) {
|
||||
this.form.shopId = Number(shopId)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消操作
|
||||
*/
|
||||
handleCancel() {
|
||||
this.$router.push('/account/shops')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-new {
|
||||
padding: 20px;
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.product-form-card {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #303133;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.product-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 单选组左对齐,和输入框对齐 */
|
||||
.product-form .el-form-item .el-radio-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 让单选组整体与输入框内容左边缘对齐(考虑到Element默认label-width=120px,
|
||||
输入框内部有一定内边距,这里为内容区域添加与输入框相同的内边距) */
|
||||
.product-form .align-like-input .el-form-item__content {
|
||||
padding-left: 15px; /* 与 el-input 默认内边距对齐 */
|
||||
}
|
||||
|
||||
.unit-text {
|
||||
margin-left: 10px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 操作按钮:居中对齐,等宽美观 */
|
||||
.actions-row .el-form-item__content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
/* display: grid; */
|
||||
grid-auto-flow: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-actions .el-button {
|
||||
min-width: auto; /* 自适应文字宽度 */
|
||||
white-space: nowrap;
|
||||
padding: 8px 20px !important;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
/* 隐藏数字输入框的上下箭头(各浏览器兼容) */
|
||||
::v-deep input[type='number']::-webkit-outer-spin-button,
|
||||
::v-deep input[type='number']::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
::v-deep input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.product-new {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.product-form-card {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__content{
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
406
power_leasing/src/views/account/products.vue
Normal file
406
power_leasing/src/views/account/products.vue
Normal file
@@ -0,0 +1,406 @@
|
||||
<template>
|
||||
<div class="account-products">
|
||||
<!-- 标题与操作栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="left-area">
|
||||
<h2 class="page-title">商品列表</h2>
|
||||
</div>
|
||||
<div class="right-area">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="输入币种或算法关键字后回车/搜索"
|
||||
size="small"
|
||||
clearable
|
||||
class="mr-12"
|
||||
style="width: 280px"
|
||||
@keyup.enter.native="handleSearch"
|
||||
@clear="handleClear"
|
||||
/>
|
||||
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
|
||||
<el-button size="small" class="ml-8" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" />
|
||||
<el-table-column prop="coin" label="币种" width="100" />
|
||||
<el-table-column prop="priceRange" label="价格范围" width="150" />
|
||||
<!-- <el-table-column label="算力" min-width="140">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.power }} {{ scope.row.unit }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="algorithm" label="算法" min-width="120" />
|
||||
<!-- <el-table-column prop="electricityBill" label="电费" width="100" /> -->
|
||||
<!-- <el-table-column prop="incomeRate" label="收益率" width="100" /> -->
|
||||
<el-table-column prop="type" label="商品类型" width="130">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 1 ? 'success' : 'warning'">
|
||||
{{ scope.row.type === 1 ? '算力套餐' : '挖矿机器' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="state" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.state === 1 ? 'info' : 'success'">
|
||||
{{ scope.row.state === 1 ? '下架' : '上架' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="220">
|
||||
<template #default="scope">
|
||||
<el-button type="text" size="small" @click="handleView(scope.row)">详情</el-button>
|
||||
<el-button type="text" size="small" @click="handleEdit(scope.row)">修改</el-button>
|
||||
<el-button type="text" size="small" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button type="text" size="small" @click="handleAddMachine(scope.row)">添加出售机器</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:current-page.sync="pagination.pageNum"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size.sync="pagination.pageSize"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<el-dialog :visible.sync="editDialog.visible" :close-on-click-modal="false" width="620px" :title="'编辑商品 - ' + ((editDialog.form && editDialog.form.name) ? editDialog.form.name : '')">
|
||||
<el-form v-if="editDialog.form" :model="editDialog.form" label-width="100px" ref="editForm" class="edit-form">
|
||||
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="editDialog.form.name" maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="图片路径">
|
||||
<el-input v-model="editDialog.form.image" />
|
||||
</el-form-item> -->
|
||||
|
||||
|
||||
<el-form-item label="状态" class="align-like-input">
|
||||
<el-radio-group v-model="editDialog.form.state">
|
||||
<el-radio :label="0">上架</el-radio>
|
||||
<el-radio :label="1">下架</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input type="textarea" :rows="4" v-model="editDialog.form.description" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editDialog.saving" @click="handleSaveEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 商品列表管理页(账户侧)
|
||||
* - 表格展示,通过 getProductList 获取数据
|
||||
* - 支持查看详情、修改、删除
|
||||
*/
|
||||
import { getProductList, updateProduct, deleteProduct, getMachineInfo} from '../../api/products'
|
||||
|
||||
export default {
|
||||
name: 'AccountProducts',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
searchKeyword: '',
|
||||
tableData: [],
|
||||
pagination: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
},
|
||||
coinOptions: [],
|
||||
editDialog: {
|
||||
visible: false,
|
||||
saving: false,
|
||||
form: null,
|
||||
},
|
||||
total: 0,
|
||||
userEmail:"",
|
||||
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initOptions()
|
||||
this.fetchTableData()
|
||||
},
|
||||
methods: {
|
||||
/** 初始化筛选选项 */
|
||||
initOptions() {
|
||||
try {
|
||||
// 动态导入,避免打包时循环引用
|
||||
const { coinList } = require('../../utils/coinList')
|
||||
this.coinOptions = Array.isArray(coinList) ? coinList : []
|
||||
} catch (e) {
|
||||
this.coinOptions = []
|
||||
}
|
||||
},
|
||||
async fetchMachineInfo(params) {
|
||||
const res = await getMachineInfo(params)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.editDialog.form = res.data
|
||||
console.log(res.data,'res.data');
|
||||
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取商品表格数据
|
||||
*/
|
||||
async fetchTableData() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 按“单一关键字”搜索:若关键字命中币种,则只传 coin;否则只传 algorithm
|
||||
const keyword = (this.searchKeyword || '').trim()
|
||||
let coinParam
|
||||
let algorithmParam
|
||||
if (keyword) {
|
||||
const lower = keyword.toLowerCase()
|
||||
const hitCoin = (this.coinOptions || []).some(c =>
|
||||
(c.value && String(c.value).toLowerCase() === lower) ||
|
||||
(c.label && String(c.label).toLowerCase() === lower)
|
||||
)
|
||||
if (hitCoin) {
|
||||
coinParam = keyword
|
||||
} else {
|
||||
algorithmParam = keyword
|
||||
}
|
||||
}
|
||||
try {
|
||||
this.userEmail=JSON.parse(localStorage.getItem('userEmail'))
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
}
|
||||
const params = {
|
||||
pageNum: this.pagination.pageNum,
|
||||
pageSize: this.pagination.pageSize,
|
||||
coin: coinParam || undefined,
|
||||
algorithm: algorithmParam || undefined,
|
||||
userEmail: this.userEmail
|
||||
}
|
||||
const res = await getProductList(params)
|
||||
// 兼容不同返回结构
|
||||
const list = res?.data?.records || res?.data?.list || res?.rows || res?.list || res?.data || []
|
||||
|
||||
this.tableData = Array.isArray(list) ? list : []
|
||||
this.total= res.total
|
||||
console.log(this.tableData)
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败', error)
|
||||
console.log('获取商品列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/** 搜索 */
|
||||
handleSearch() {
|
||||
this.pagination.pageNum = 1
|
||||
this.fetchTableData()
|
||||
},
|
||||
|
||||
/** 重置 */
|
||||
handleReset() {
|
||||
this.searchKeyword = ''
|
||||
this.pagination.pageNum = 1
|
||||
this.pagination.pageSize = 10
|
||||
this.fetchTableData()
|
||||
},
|
||||
|
||||
|
||||
|
||||
/** 查看详情 */
|
||||
handleView(row) {
|
||||
if (!row || !row.id) {
|
||||
this.$message({
|
||||
message: '缺少商品ID',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$router.push(`/account/product-detail/${row.id}`)
|
||||
},
|
||||
|
||||
/** 编辑 */
|
||||
handleEdit(row) {
|
||||
this.editDialog.form = { ...row }
|
||||
this.editDialog.visible = true
|
||||
},
|
||||
|
||||
/** 保存编辑 */
|
||||
async handleSaveEdit() {
|
||||
if (!this.editDialog.form) return
|
||||
this.editDialog.saving = true
|
||||
try {
|
||||
// 简单内容校验:不能为空且不能全是空格(不影响其它逻辑)
|
||||
const notEmpty = (v) => typeof v === 'string' && v.trim().length > 0
|
||||
const name = String(this.editDialog.form.name || '')
|
||||
const description = String(this.editDialog.form.description || '')
|
||||
if (!notEmpty(name)) {
|
||||
this.$message.warning('名称不能为空或全是空格')
|
||||
this.editDialog.saving = false
|
||||
return
|
||||
}
|
||||
if (!notEmpty(description)) {
|
||||
this.$message.warning('描述不能为空或全是空格')
|
||||
this.editDialog.saving = false
|
||||
return
|
||||
}
|
||||
// 去除首尾空格后再提交
|
||||
this.editDialog.form.name = name.trim()
|
||||
this.editDialog.form.description = description.trim()
|
||||
// 仅提交指定字段
|
||||
const payload = {
|
||||
id: this.editDialog.form.id,
|
||||
shopId: this.editDialog.form.shopId,
|
||||
name: this.editDialog.form.name,
|
||||
image: this.editDialog.form.image,
|
||||
coin: this.editDialog.form.coin,
|
||||
description: this.editDialog.form.description,
|
||||
state: this.editDialog.form.state,
|
||||
}
|
||||
const res = await updateProduct(payload)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message({
|
||||
message: '保存成功',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
this.editDialog.visible = false
|
||||
this.fetchTableData()
|
||||
} else {
|
||||
this.$message({
|
||||
message: res?.msg || '保存失败',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存商品失败', error)
|
||||
console.log('保存失败')
|
||||
} finally {
|
||||
this.editDialog.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
/** 删除 */
|
||||
handleDelete(row) {
|
||||
if (!row || !row.id) return
|
||||
this.$confirm('确认删除该商品吗?删除后不可恢复', '提示', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteProduct(row.id)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message.success('删除成功')
|
||||
// 若当前页仅一条且删除后为空,回退一页
|
||||
if (this.tableData.length === 1 && this.pagination.pageNum > 1) {
|
||||
this.pagination.pageNum -= 1
|
||||
}
|
||||
this.fetchTableData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除商品失败', error)
|
||||
console.log('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
/** 分页 - 每页条数变化 */
|
||||
handleSizeChange(size) {
|
||||
this.pagination.pageSize = size
|
||||
this.pagination.pageNum = 1
|
||||
this.fetchTableData()
|
||||
},
|
||||
|
||||
/** 分页 - 当前页变化 */
|
||||
handleCurrentChange(page) {
|
||||
this.pagination.pageNum = page
|
||||
this.fetchTableData()
|
||||
},
|
||||
|
||||
/** 清空搜索框时恢复默认列表 */
|
||||
handleClear() {
|
||||
this.searchKeyword = ''
|
||||
this.pagination.pageNum = 1
|
||||
this.fetchTableData()
|
||||
},
|
||||
|
||||
/** 跳转添加出售机器 */
|
||||
handleAddMachine(row) {
|
||||
if (!row || !row.id) {
|
||||
this.$message.warning('缺少商品ID')
|
||||
return
|
||||
}
|
||||
this.$router.push({ path: '/account/product-machine-add', query: { productId: row.id, coin: row.coin, name: row.name } })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.account-products {
|
||||
padding: 4px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.left-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.right-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mr-12 { margin-right: 12px; }
|
||||
.ml-8 { margin-left: 8px; }
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 编辑弹窗内:让下拉/单选与输入框左边缘对齐 */
|
||||
.edit-form .align-like-input .el-form-item__content {
|
||||
padding-left: 12px; /* 对齐到上方 el-input 的内边距视觉效果 */
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__content{
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
234
power_leasing/src/views/account/purchased.vue
Normal file
234
power_leasing/src/views/account/purchased.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="account-purchased">
|
||||
<div class="toolbar">
|
||||
<div class="left-area">
|
||||
<h2 class="page-title">已购商品</h2>
|
||||
</div>
|
||||
<!-- <div class="right-area">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="输入币种或算法后搜索(可留空)"
|
||||
size="small"
|
||||
clearable
|
||||
class="mr-12"
|
||||
style="width: 280px"
|
||||
@keyup.enter.native="handleSearch"
|
||||
@clear="handleClear"
|
||||
/>
|
||||
<el-button type="primary" size="small" @click="handleSearch"
|
||||
>搜索</el-button
|
||||
>
|
||||
<el-button size="small" class="ml-8" @click="handleReset"
|
||||
>重置</el-button
|
||||
>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="userId" label="用户" width="180" />
|
||||
|
||||
<el-table-column prop="productMachineId" label="机器ID" width="80" />
|
||||
<!-- <el-table-column
|
||||
prop="purchasedComputingPower"
|
||||
label="购买算力"
|
||||
min-width="120"
|
||||
|
||||
/> -->
|
||||
|
||||
<el-table-column prop="type" label="类型" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 1 ? 'success' : 'info'">
|
||||
{{ scope.row.type === 1 ? "算力套餐" : "挖矿机器" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column prop="currentIncome" label="当前收入" min-width="120" />
|
||||
<el-table-column
|
||||
prop="currentUsdtIncome"
|
||||
label="当前USDT收入"
|
||||
min-width="140"
|
||||
/> -->
|
||||
<el-table-column
|
||||
prop="estimatedEndIncome"
|
||||
label="预计总收益"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="estimatedEndUsdtIncome"
|
||||
label="预计USDT总收益"
|
||||
min-width="160"
|
||||
/>
|
||||
<el-table-column prop="startTime" label="开始时间" min-width="160" >
|
||||
<template #default="scope">
|
||||
<span>{{ formatDateTime(scope.row.startTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="endTime" label="结束时间" min-width="160" >
|
||||
<template #default="scope">
|
||||
<span>{{ formatDateTime(scope.row.endTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 0 ? 'success' : 'info'">
|
||||
{{ scope.row.status === 0 ? "运行中" : "已过期" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="120">
|
||||
<template #default="scope">
|
||||
<el-button type="text" size="small" @click="handleView(scope.row)"
|
||||
>详情</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:current-page.sync="pagination.pageNum"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size.sync="pagination.pageSize"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOwnedList } from "../../api/products";
|
||||
import { coinList } from "../../utils/coinList";
|
||||
|
||||
export default {
|
||||
name: "AccountPurchased",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
searchKeyword: "",
|
||||
tableData: [],
|
||||
pagination: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
|
||||
},
|
||||
coins: coinList || [],
|
||||
total:0,
|
||||
currentPage:1,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchTableData(this.pagination);
|
||||
},
|
||||
methods: {
|
||||
async fetchTableData(params) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await getOwnedList(params);
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.tableData = res.rows;
|
||||
this.total = res.total;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("获取已购商品失败", e);
|
||||
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.pagination.pageNum = 1;
|
||||
this.fetchTableData();
|
||||
},
|
||||
handleReset() {
|
||||
this.searchKeyword = "";
|
||||
this.pagination.pageNum = 1;
|
||||
this.pagination.pageSize = 10;
|
||||
this.fetchTableData(this.pagination);
|
||||
},
|
||||
handleClear() {
|
||||
this.searchKeyword = "";
|
||||
this.pagination.pageNum = 1;
|
||||
this.fetchTableData(this.pagination);
|
||||
},
|
||||
handleSizeChange(size) {
|
||||
this.pagination.pageSize = size;
|
||||
this.pagination.pageNum = 1;
|
||||
this.fetchTableData(this.pagination);
|
||||
},
|
||||
handleCurrentChange(page) {
|
||||
this.pagination.pageNum = page;
|
||||
this.fetchTableData(this.pagination);
|
||||
},
|
||||
handleView(row) {
|
||||
// 跳转到已购商品详情页面
|
||||
this.$router.push({
|
||||
name: 'PurchasedDetail',
|
||||
params: { id: row.id }
|
||||
})
|
||||
},
|
||||
formatDateTime(value) {
|
||||
if (!value) return '—'
|
||||
try {
|
||||
const str = String(value)
|
||||
return str.includes('T') ? str.replace('T', ' ') : str
|
||||
} catch (e) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.account-purchased {
|
||||
padding: 4px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.left-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.right-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mr-12 {
|
||||
margin-right: 12px;
|
||||
}
|
||||
.ml-8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.thumb {
|
||||
width: 72px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
217
power_leasing/src/views/account/purchasedDetail.vue
Normal file
217
power_leasing/src/views/account/purchasedDetail.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="purchased-detail-page">
|
||||
<h2 class="title">已购商品详情</h2>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else>
|
||||
<!-- 基本信息 -->
|
||||
<el-card class="section">
|
||||
<div class="sub-title">基本信息</div>
|
||||
<div class="row">
|
||||
<span class="label">用户:</span>
|
||||
<span class="value mono">{{ detail.userId || '—' }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">订单项ID:</span>
|
||||
<span class="value mono">{{ detail.orderItemId || '—' }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">机器ID:</span>
|
||||
<span class="value mono">{{ detail.productMachineId || '—' }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">商品类型:</span>
|
||||
<span class="value">
|
||||
<el-tag :type="detail.type === 1 ? 'success' : 'info'">
|
||||
{{ detail.type === 1 ? "算力套餐" : "挖矿机器" }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">状态:</span>
|
||||
<span class="value">
|
||||
<el-tag :type="detail.status === 0 ? 'success' : 'info'">
|
||||
{{ detail.status === 0 ? "运行中" : "已过期" }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row" v-show="detail.type === 1">
|
||||
<span class="label" >购买算力:</span>
|
||||
<span class="value strong">{{ detail.purchasedComputingPower }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">购买时间:</span>
|
||||
<span class="value">{{ formatDateTime(detail.createTime) }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">开始时间:</span>
|
||||
<span class="value">{{ formatDateTime(detail.startTime) }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">结束时间:</span>
|
||||
<span class="value">{{ formatDateTime(detail.endTime) }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 收益信息 -->
|
||||
<el-card class="section" style="margin-top: 12px;">
|
||||
<div class="sub-title">收益信息</div>
|
||||
<div class="row">
|
||||
<span class="label">当前实时算力:</span>
|
||||
<span class="value strong">{{ detail.currentComputingPower || '0' }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">币种收益:</span>
|
||||
<span class="value strong">{{ detail.currentIncome || '0' }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">当前USDT收益:</span>
|
||||
<span class="value strong">{{ detail.currentUsdtIncome || '0' }} USDT</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">预估结束总收益:</span>
|
||||
<span class="value strong">{{ detail.estimatedEndIncome || '0' }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">预估结束USDT总收益:</span>
|
||||
<span class="value strong">{{ detail.estimatedEndUsdtIncome || '0' }} USDT</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions">
|
||||
<el-button @click="$router.back()">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOwnedList,getOwnedById } from '../../api/products'
|
||||
|
||||
export default {
|
||||
name: 'PurchasedDetail',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
detail: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
const paramsId = this.$route.params.id
|
||||
if (!paramsId) {
|
||||
this.$message({
|
||||
message: '订单项ID缺失,请重新进入',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true
|
||||
const res = await getOwnedById({id:paramsId})
|
||||
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 从列表中查找对应的订单项
|
||||
// const items = res.rows || []
|
||||
// const targetItem = items.find(item => item.orderItemId == orderItemId)
|
||||
this.detail = res.data
|
||||
// if (targetItem) {
|
||||
// this.detail = targetItem
|
||||
// } else {
|
||||
// this.$message({
|
||||
// message: '未找到对应的商品信息',
|
||||
// type: 'error',
|
||||
// showClose: true
|
||||
// })
|
||||
// }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取已购商品详情失败', e)
|
||||
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
formatDateTime(value) {
|
||||
if (!value) return '—'
|
||||
try {
|
||||
const str = String(value)
|
||||
return str.includes('T') ? str.replace('T', ' ') : str
|
||||
} catch (e) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.purchased-detail-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 12px 0;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
min-width: 170px;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
/* background: palegoldenrod; */
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
.value.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.value.strong {
|
||||
font-weight: 700;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
1034
power_leasing/src/views/account/rechargeRecord.vue
Normal file
1034
power_leasing/src/views/account/rechargeRecord.vue
Normal file
File diff suppressed because it is too large
Load Diff
168
power_leasing/src/views/account/shopConfig.vue
Normal file
168
power_leasing/src/views/account/shopConfig.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<h2 class="panel-title page-title">钱包绑定</h2>
|
||||
<div class="panel-body">
|
||||
<el-form :model="form" label-width="120px" class="config-form">
|
||||
<el-form-item label="适用商品">
|
||||
<el-select v-model="form.productId" placeholder="请选择商品">
|
||||
<el-option :value="0" label="全部商品" />
|
||||
<el-option
|
||||
v-for="p in productOptions"
|
||||
:key="p.id"
|
||||
:value="p.id"
|
||||
:label="`${p.id} - ${p.name}`"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收款钱包地址">
|
||||
<el-input
|
||||
v-model="form.payAddress"
|
||||
placeholder="示例:nexa:nqtsq5g50jkkmklvjyaflg46c4nwuy46z9gzswqe3l0csc7g"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="币种类型">
|
||||
<el-radio-group v-model="form.payType" class="radio-group">
|
||||
<el-radio :label="0">虚拟币</el-radio>
|
||||
<el-radio :label="1">稳定币</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付币种">
|
||||
<el-select v-model="form.payCoin" placeholder="请选择支付币种" filterable clearable>
|
||||
<el-option v-for="c in coinOptions" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSave">保存配置</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addShopConfig ,getMyShop} from '@/api/shops'
|
||||
|
||||
// 币种集合
|
||||
const VIRTUAL_COINS = ['nexa', 'rxd', 'dgbo', 'dgbq', 'dgbs', 'alph', 'enx', 'grs', 'mona']
|
||||
const STABLE_COINS = ['usdt', 'usdc', 'busd']
|
||||
|
||||
export default {
|
||||
name: 'AccountShopConfig',
|
||||
data() {
|
||||
return {
|
||||
VIRTUAL_COINS,
|
||||
STABLE_COINS,
|
||||
productOptions: [],
|
||||
form: {
|
||||
payAddress: 'nexa:nqtsq5g50jkkmklvjyaflg46c4nwuy46z9gzswqe3l0csc7g',
|
||||
payCoin: '',
|
||||
payType: 0, // 0 虚拟币 1 稳定币
|
||||
productId: 0, // 0 代表所有商品
|
||||
shopId: 0
|
||||
},
|
||||
shop: {
|
||||
id: 0,
|
||||
name: '',
|
||||
image: '',
|
||||
description: '',
|
||||
del: true,
|
||||
state: 0
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMyShop()
|
||||
},
|
||||
methods: {
|
||||
async fetchMyShop() {
|
||||
try {
|
||||
const res = await getMyShop()
|
||||
// 预期格式:{"code":0,"data":{"del":true,"description":"","id":0,"image":"","name":"","state":0},"msg":""}
|
||||
if (res && (res.code === 0 || res.code === 200) && res.data) {
|
||||
this.shop = {
|
||||
id: res.data.id,
|
||||
name: res.data.name,
|
||||
image: res.data.image,
|
||||
description: res.data.description,
|
||||
del: !!res.data.del,
|
||||
state: Number(res.data.state || 0)
|
||||
}
|
||||
this.form.shopId = this.shop.id
|
||||
} else {
|
||||
this.$message.warning(res.msg || '未获取到店铺数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺信息失败:', error)
|
||||
|
||||
} finally {
|
||||
this.loaded = true
|
||||
}
|
||||
},
|
||||
|
||||
async addShopConfig(params) {
|
||||
const res = await addShopConfig(params)
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
this.$message.success('已保存配置(示例)')
|
||||
} else {
|
||||
this.$message.error(res.msg || '保存失败')
|
||||
}
|
||||
},
|
||||
|
||||
handleSave() {
|
||||
this.form.shopId = this.shop.id
|
||||
if (!this.form.shopId) {
|
||||
this.$message.warning(`未查询到店铺信息`)
|
||||
return
|
||||
}
|
||||
this.addShopConfig(this.form)
|
||||
|
||||
},
|
||||
handleReset() {
|
||||
this.form = { payAddress: '', payCoin: '', payType: 0, productId: 0 }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 根据币种类型动态展示可选币种
|
||||
coinOptions() {
|
||||
return this.form.payType === 1 ? STABLE_COINS : VIRTUAL_COINS
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'form.payType'(val) {
|
||||
// 切换类型时清空已选币种,避免类型与币种不匹配
|
||||
this.form.payCoin = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title { text-align: left; margin-bottom: 16px; font-size: 20px; padding-left: 4px; }
|
||||
.config-form {
|
||||
max-width: 720px;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.config-form .el-form-item { margin-bottom: 18px; }
|
||||
.config-form .el-select,
|
||||
.config-form .el-input { width: 420px; }
|
||||
.radio-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
width: 420px; /* 与上方输入框等宽 */
|
||||
height: 40px; /* 与输入框高度接近,视觉更整齐 */
|
||||
padding-left: 12px; /* 留出与输入框一致的内边距感 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tip { color: #999; font-size: 12px; margin-top: 6px; }
|
||||
</style>
|
||||
|
||||
183
power_leasing/src/views/account/shopNew.vue
Normal file
183
power_leasing/src/views/account/shopNew.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<h2 class="panel-title">新增店铺</h2>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<label class="label">店铺名称</label>
|
||||
<el-input v-model="form.name" placeholder="请输入店铺名称" :maxlength="30" show-word-limit />
|
||||
</div>
|
||||
<!-- <div class="row">
|
||||
<label class="label">主营类目</label>
|
||||
<el-input v-model="form.category" placeholder="请输入主营类目" />
|
||||
</div> -->
|
||||
<div class="row">
|
||||
<label class="label">店铺描述</label>
|
||||
<div class="textarea-wrapper">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="form.description"
|
||||
:rows="4"
|
||||
:maxlength="300"
|
||||
placeholder="请输入店铺描述"
|
||||
show-word-limit
|
||||
@input="handleDescriptionInput"
|
||||
/>
|
||||
<!-- <div class="char-count" :class="{ 'char-limit': form.description.length >= 300 }">
|
||||
{{ form.description.length }}/300
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<el-button type="primary" @click="handleCreate">创建店铺</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAddShop } from "@/api/shops";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: { name: "", description: "", image: "" },
|
||||
};
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
// 简单的emoji检测:覆盖常见表情平面与符号范围
|
||||
hasEmoji(str) {
|
||||
if (!str || typeof str !== 'string') return false
|
||||
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u2600-\u27BF]/u
|
||||
return emojiRegex.test(str)
|
||||
},
|
||||
async fetchAddShop() {
|
||||
const res = await getAddShop(this.form);
|
||||
if (res && res.code==200) {
|
||||
this.$message({
|
||||
message: '店铺创建成功',
|
||||
type: 'success',
|
||||
showClose: true
|
||||
})
|
||||
this.$router.push('/account/shops')
|
||||
}
|
||||
|
||||
},
|
||||
handleDescriptionInput(value) {
|
||||
// 确保不超过300个字符
|
||||
if (value && value.length > 300) {
|
||||
this.form.description = value.substring(0, 300)
|
||||
this.$message({
|
||||
message: '店铺描述不能超过300个字符',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
},
|
||||
handleCreate() {
|
||||
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
|
||||
if (!this.form.name || isOnlySpaces(this.form.name)) {
|
||||
this.$message({
|
||||
message: '店铺名称不能为空或全是空格',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.hasEmoji(this.form.name)) {
|
||||
this.$message({
|
||||
message: '店铺名称不能包含表情符号',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.form.name && this.form.name.length > 30) {
|
||||
this.$message({
|
||||
message: '店铺名称不能超过30个字符',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isOnlySpaces(this.form.description)) {
|
||||
this.$message({
|
||||
message: '店铺描述不能全是空格',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 检查店铺描述长度
|
||||
if (this.form.description && this.form.description.length > 300) {
|
||||
this.$message({
|
||||
message: '店铺描述不能超过300个字符',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 不允许在已有店铺时新建。这里通过路由跳转来源检查或请求前端缓存判断。
|
||||
// 更稳妥:可在进入本页前做守卫拦截;此处做一次兜底校验。
|
||||
if (this.$route.query && this.$route.query.hasShop === '1') {
|
||||
this.$message({
|
||||
message: '每个用户仅允许一个店铺,无法新建',
|
||||
type: 'warning',
|
||||
showClose: true
|
||||
});
|
||||
this.$router.replace('/account/shops');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.form.name) {
|
||||
this.$message.error('店铺名称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.fetchAddShop(this.form)
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel-title {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.label {
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.char-count.char-limit {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
45
power_leasing/src/views/account/shopSettings.vue
Normal file
45
power_leasing/src/views/account/shopSettings.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<h2 class="panel-title">店铺设置</h2>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<label class="label">店铺Logo</label>
|
||||
<el-input v-model="form.logo" placeholder="请输入Logo图片地址" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="label">联系人</label>
|
||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="label">联系电话</label>
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<el-button type="primary" @click="handleSave">保存设置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AccountShopSettings',
|
||||
data() {
|
||||
return {
|
||||
form: { logo: '', contact: '', phone: '' }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSave() {
|
||||
this.$message.success('店铺设置已保存(示例)')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel-title { margin: 0 0 12px 0; font-size: 18px; font-weight: 700; }
|
||||
.row { display: grid; grid-template-columns: 100px 1fr; gap: 12px; align-items: center; margin-bottom: 12px; }
|
||||
.label { color: #666; text-align: right; }
|
||||
</style>
|
||||
|
||||
1282
power_leasing/src/views/account/wallet.vue
Normal file
1282
power_leasing/src/views/account/wallet.vue
Normal file
File diff suppressed because it is too large
Load Diff
969
power_leasing/src/views/account/withdrawalHistory.vue
Normal file
969
power_leasing/src/views/account/withdrawalHistory.vue
Normal file
@@ -0,0 +1,969 @@
|
||||
<template>
|
||||
<div class="withdrawal-history-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">提现记录</h1>
|
||||
<p class="page-subtitle">查看您的提现申请和交易状态</p>
|
||||
</div>
|
||||
|
||||
<!-- 状态标签页 -->
|
||||
<div class="tab-container">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="提现中" name="pending">
|
||||
<div class="tab-content">
|
||||
<div class="list-header">
|
||||
<span class="list-title">提现中 ({{ total}})</span>
|
||||
<el-button type="primary" size="small" @click="refreshData">
|
||||
<i class="el-icon-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="withdrawal-list" v-loading="loading">
|
||||
<div
|
||||
v-for="item in pendingWithdrawals"
|
||||
:key="item.id"
|
||||
class="withdrawal-item pending"
|
||||
@click="showDetail(item)"
|
||||
>
|
||||
<div class="item-main">
|
||||
<div class="item-left">
|
||||
<div class="amount">{{ item.amount }} {{ item.toSymbol || 'USDT' }}</div>
|
||||
<div class="chain">{{ getChainName(item.toChain) }}</div>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<div class="status pending-status">
|
||||
<i class="el-icon-loading"></i>
|
||||
{{ getStatusText(item.status) }}
|
||||
</div>
|
||||
<div class="time">{{ formatTime(item.createTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-footer">
|
||||
<div class="footer-left">
|
||||
<span class="address">{{ formatAddress(item.toAddress) }}</span>
|
||||
<span class="tx-hash" v-if="item.txHash">
|
||||
<i class="el-icon-link"></i>
|
||||
{{ formatAddress(item.txHash) }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="pendingWithdrawals.length === 0" class="empty-state">
|
||||
<i class="el-icon-document"></i>
|
||||
<p>暂无提现中的记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="提现成功" name="success">
|
||||
<div class="tab-content">
|
||||
<div class="list-header">
|
||||
<span class="list-title">提现成功 ({{total }})</span>
|
||||
<el-button type="primary" size="small" @click="refreshData">
|
||||
<i class="el-icon-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="withdrawal-list" v-loading="loading">
|
||||
<div
|
||||
v-for="item in successWithdrawals"
|
||||
:key="item.id"
|
||||
class="withdrawal-item success"
|
||||
@click="showDetail(item)"
|
||||
>
|
||||
<div class="item-main">
|
||||
<div class="item-left">
|
||||
<div class="amount">{{ item.amount }} {{ item.toSymbol || 'USDT' }}</div>
|
||||
<div class="chain">{{ getChainName(item.toChain) }}</div>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<div class="status success-status">
|
||||
<i class="el-icon-check"></i>
|
||||
{{ getStatusText(item.status) }}
|
||||
</div>
|
||||
<div class="time">{{ formatTime(item.createTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-footer">
|
||||
<div class="footer-left">
|
||||
<span class="address">{{ formatAddress(item.toAddress) }}</span>
|
||||
<span class="tx-hash" v-if="item.txHash">
|
||||
<i class="el-icon-link"></i>
|
||||
{{ formatAddress(item.txHash) }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="successWithdrawals.length === 0" class="empty-state">
|
||||
<i class="el-icon-document"></i>
|
||||
<p>暂无提现成功的记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="提现失败" name="failed">
|
||||
<div class="tab-content">
|
||||
<div class="list-header">
|
||||
<span class="list-title">提现失败 ({{ total }})</span>
|
||||
<el-button type="primary" size="small" @click="refreshData">
|
||||
<i class="el-icon-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="withdrawal-list" v-loading="loading">
|
||||
<div
|
||||
v-for="item in failedWithdrawals"
|
||||
:key="item.id"
|
||||
class="withdrawal-item failed"
|
||||
@click="showDetail(item)"
|
||||
>
|
||||
<div class="item-main">
|
||||
<div class="item-left">
|
||||
<div class="amount">{{ item.amount }} {{ item.toSymbol || 'USDT' }}</div>
|
||||
<div class="chain">{{ getChainName(item.toChain) }}</div>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<div class="status failed-status">
|
||||
<i class="el-icon-close"></i>
|
||||
{{ getStatusText(item.status) }}
|
||||
</div>
|
||||
<div class="time">{{ formatTime(item.createTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-footer">
|
||||
<div class="footer-left">
|
||||
<span class="address">{{ formatAddress(item.toAddress) }}</span>
|
||||
<span class="tx-hash" v-if="item.txHash">
|
||||
<i class="el-icon-link"></i>
|
||||
{{ formatAddress(item.txHash) }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="el-icon-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="failedWithdrawals.length === 0" class="empty-state">
|
||||
<i class="el-icon-document"></i>
|
||||
<p>暂无提现失败的记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24" style="display: flex; justify-content: center">
|
||||
<el-pagination
|
||||
style="margin: 0 auto; margin-top: 10px"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page.sync="currentPage"
|
||||
:page-sizes="pageSizes"
|
||||
:page-size="pagination.pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
>
|
||||
</el-pagination>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
title="提现详情"
|
||||
:visible.sync="detailDialogVisible"
|
||||
width="600px"
|
||||
@close="closeDetail"
|
||||
>
|
||||
<div v-if="selectedItem" class="detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">基本信息</h3>
|
||||
<div class="detail-list">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">提现ID</span>
|
||||
<span class="detail-value">{{ selectedItem.id }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">提现金额</span>
|
||||
<span class="detail-value amount">{{ selectedItem.amount }} {{ selectedItem.toSymbol || 'USDT' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">区块链网络</span>
|
||||
<span class="detail-value">{{ getChainName(selectedItem.toChain) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">提现状态</span>
|
||||
<span class="detail-value">
|
||||
<el-tag :type="getStatusType(selectedItem.status)">
|
||||
{{ getStatusText(selectedItem.status) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 地址信息 -->
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">地址信息</h3>
|
||||
<div class="detail-list">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">收款地址</span>
|
||||
<div class="address-container">
|
||||
<span class="detail-value address">{{ selectedItem.toAddress }}</span>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="copyAddress(selectedItem.toAddress)"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v-if="selectedItem.txHash" -->
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">交易哈希</span>
|
||||
<div class="address-container">
|
||||
<span class="detail-value address">{{ selectedItem.txHash }}</span>
|
||||
<el-button
|
||||
v-if="selectedItem.txHash"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="copyAddress(selectedItem.txHash)"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
<!-- <el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="viewOnExplorer(selectedItem.txHash, selectedItem.toChain)"
|
||||
v-if="selectedItem.txHash"
|
||||
>
|
||||
查看
|
||||
</el-button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">时间信息</h3>
|
||||
<div class="detail-list">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">提现时间</span>
|
||||
<span class="detail-value">{{ formatFullTime(selectedItem.createTime) }}</span>
|
||||
</div>
|
||||
<div class="detail-row" v-if="selectedItem.updateTime">
|
||||
<span class="detail-label">完成时间</span>
|
||||
<span class="detail-value">{{ formatFullTime(selectedItem.updateTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="closeDetail">关闭</el-button>
|
||||
<!-- <el-button type="primary" @click="refreshData" v-if="selectedItem && selectedItem.status === 2">
|
||||
刷新状态
|
||||
</el-button> -->
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { balanceWithdrawList } from '../../api/wallet'
|
||||
export default {
|
||||
name: 'WithdrawalHistory',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'pending',
|
||||
detailDialogVisible: false,
|
||||
selectedItem: null,
|
||||
|
||||
// 提现记录数据
|
||||
withdrawalRecords: [
|
||||
|
||||
// {
|
||||
// id: 1,
|
||||
// amount: 100,
|
||||
// toSymbol: 'USDT',
|
||||
// toChain: 'tron',
|
||||
// status: 2,
|
||||
// createTime: '2024-01-15 14:30:25',
|
||||
// toAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
|
||||
// updateTime: '2024-01-15 14:30:25',
|
||||
// fromAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
|
||||
// fromChain: 'tron',
|
||||
// fromSymbol: 'USDT',
|
||||
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// amount: 100,
|
||||
// toSymbol: 'USDT',
|
||||
// toChain: 'tron',
|
||||
// status: 1,
|
||||
// createTime: '2024-01-15 14:30:25',
|
||||
// toAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
|
||||
// updateTime: '2024-01-15 14:30:25',
|
||||
// fromAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
|
||||
// fromChain: 'tron',
|
||||
// fromSymbol: 'USDT',
|
||||
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// amount: 100,
|
||||
// toSymbol: 'USDT',
|
||||
// toChain: 'tron',
|
||||
// status: 0,
|
||||
// createTime: '2024-01-15 14:30:25',
|
||||
// toAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
|
||||
// updateTime: '2024-01-15 14:30:25',
|
||||
// fromAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
|
||||
// fromChain: 'tron',
|
||||
// fromSymbol: 'USDT',
|
||||
|
||||
// },
|
||||
|
||||
|
||||
],
|
||||
|
||||
// 分页和筛选参数
|
||||
pagination: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
|
||||
totalPage: 0
|
||||
},
|
||||
|
||||
// 加载状态
|
||||
loading: false,
|
||||
|
||||
// 状态筛选
|
||||
statusFilter: '',
|
||||
total: 0,
|
||||
pageSizes: [10, 20, 50],
|
||||
currentPage: 1,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* 提现中的记录 (status = 2)
|
||||
*/
|
||||
pendingWithdrawals() {
|
||||
return this.withdrawalRecords.filter(item => item.status === 2)
|
||||
},
|
||||
|
||||
/**
|
||||
* 提现成功的记录 (status = 1)
|
||||
*/
|
||||
successWithdrawals() {
|
||||
return this.withdrawalRecords.filter(item => item.status === 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* 提现失败的记录 (status = 0)
|
||||
*/
|
||||
failedWithdrawals() {
|
||||
return this.withdrawalRecords.filter(item => item.status === 0)
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 设置默认选中提现中状态
|
||||
this.activeTab = 'pending'
|
||||
this.statusFilter = 2
|
||||
this.loadWithdrawalRecords()
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 获取提现记录列表
|
||||
* @param {Object} params - 请求参数
|
||||
* @param {number} params.pageNum - 当前页码,默认为1
|
||||
* @param {number} params.pageSize - 每页条数,默认为20
|
||||
* @param {number} params.status - 记录状态:0-提现失败,1-提现成功,2-提现中
|
||||
*/
|
||||
async fetchBalanceWithdrawList(params = {}) {
|
||||
try {
|
||||
// 设置默认参数
|
||||
const requestParams = {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
...params
|
||||
}
|
||||
|
||||
console.log('获取提现记录参数:', requestParams)
|
||||
|
||||
const res = await balanceWithdrawList(requestParams)
|
||||
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 更新提现记录数据
|
||||
this.withdrawalRecords = res.rows || []
|
||||
|
||||
// 更新分页信息
|
||||
|
||||
this.pagination.totalPage = res.totalPage || 0
|
||||
this.total = res.total || 0
|
||||
|
||||
console.log('提现记录获取成功:', {
|
||||
records: this.withdrawalRecords,
|
||||
pagination: this.pagination
|
||||
})
|
||||
} else {
|
||||
this.$message({
|
||||
message: res?.msg || '获取提现记录失败',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提现记录失败:', error)
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载提现记录
|
||||
*/
|
||||
async loadWithdrawalRecords() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
pageNum: this.pagination.pageNum,
|
||||
pageSize: this.pagination.pageSize
|
||||
}
|
||||
|
||||
// 如果选择了状态筛选,添加状态参数
|
||||
if (this.statusFilter !== '') {
|
||||
params.status = this.statusFilter
|
||||
}
|
||||
|
||||
await this.fetchBalanceWithdrawList(params)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 标签页切换
|
||||
*/
|
||||
handleTabClick(tab) {
|
||||
this.activeTab = tab.name
|
||||
|
||||
// 根据标签页设置状态筛选
|
||||
if (tab.name === 'pending') {
|
||||
this.statusFilter = 2 // 提现中
|
||||
} else if (tab.name === 'success') {
|
||||
this.statusFilter = 1 // 提现成功
|
||||
} else if (tab.name === 'failed') {
|
||||
this.statusFilter = 0 // 提现失败
|
||||
}
|
||||
this.currentPage = 1;
|
||||
this.pagination.pageSize = 10;
|
||||
// 重置分页并重新加载数据
|
||||
this.pagination.pageNum = 1
|
||||
this.loadWithdrawalRecords()
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示详情
|
||||
*/
|
||||
showDetail(item) {
|
||||
this.selectedItem = item
|
||||
this.detailDialogVisible = true
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭详情
|
||||
*/
|
||||
closeDetail() {
|
||||
this.detailDialogVisible = false
|
||||
this.selectedItem = null
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取链名称
|
||||
*/
|
||||
getChainName(chain) {
|
||||
const chainNames = {
|
||||
tron: 'Tron (TRC20)',
|
||||
ethereum: 'Ethereum (ERC20)',
|
||||
bsc: 'BSC (BEP20)',
|
||||
polygon: 'Polygon (MATIC)'
|
||||
}
|
||||
return chainNames[chain] || chain
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取状态类型
|
||||
*/
|
||||
getStatusType(status) {
|
||||
const statusTypeMap = {
|
||||
0: 'danger', // 提现失败
|
||||
1: 'success', // 提现成功
|
||||
2: 'warning' // 提现中
|
||||
}
|
||||
return statusTypeMap[status] || 'info'
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化地址
|
||||
*/
|
||||
formatAddress(address) {
|
||||
if (!address) return ''
|
||||
return address.length > 20 ? `${address.slice(0, 10)}...${address.slice(-10)}` : address
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
if (diff < 60000) { // 1分钟内
|
||||
return '刚刚'
|
||||
} else if (diff < 3600000) { // 1小时内
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
} else if (diff < 86400000) { // 1天内
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
} else {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化完整时间
|
||||
*/
|
||||
formatFullTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
return new Date(timeStr).toLocaleString('zh-CN')
|
||||
},
|
||||
|
||||
/**
|
||||
* 复制地址
|
||||
*/
|
||||
copyAddress(address) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(address).then(() => {
|
||||
this.$message.success('地址已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
this.fallbackCopyAddress(address)
|
||||
})
|
||||
} else {
|
||||
this.fallbackCopyAddress(address)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 备用复制方法
|
||||
*/
|
||||
fallbackCopyAddress(address) {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = address
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
this.$message.success('地址已复制到剪贴板')
|
||||
} catch (err) {
|
||||
this.$message.error('复制失败,请手动复制')
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
},
|
||||
|
||||
/**
|
||||
* 在浏览器中查看交易
|
||||
*/
|
||||
viewOnExplorer(txHash, chain) {
|
||||
const explorers = {
|
||||
tron: `https://tronscan.org/#/transaction/${txHash}`,
|
||||
ethereum: `https://etherscan.io/tx/${txHash}`,
|
||||
bsc: `https://bscscan.com/tx/${txHash}`,
|
||||
polygon: `https://polygonscan.com/tx/${txHash}`
|
||||
}
|
||||
|
||||
const url = explorers[chain]
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
} else {
|
||||
this.$message.error('不支持的区块链网络')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
refreshData() {
|
||||
this.loadWithdrawalRecords()
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化提现状态显示
|
||||
*/
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
0: '提现失败',
|
||||
1: '提现成功',
|
||||
2: '提现中'
|
||||
}
|
||||
return statusMap[status] || '未知状态'
|
||||
},
|
||||
|
||||
|
||||
handleSizeChange(val) {
|
||||
console.log(`每页 ${val} 条`);
|
||||
this.pagination.pageSize = val;
|
||||
this.pagination.pageNum = 1;
|
||||
this.currentPage = 1;
|
||||
this.loadWithdrawalRecords();
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
console.log(`当前页: ${val}`);
|
||||
this.pagination.pageNum = val;
|
||||
this.loadWithdrawalRecords();
|
||||
},
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器 */
|
||||
.withdrawal-history-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 标签页容器 */
|
||||
.tab-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 列表头部 */
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 提现列表 */
|
||||
.withdrawal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.withdrawal-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.withdrawal-item:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #409eff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.withdrawal-item.pending {
|
||||
border-left: 4px solid #e6a23c;
|
||||
}
|
||||
|
||||
.withdrawal-item.success {
|
||||
border-left: 4px solid #67c23a;
|
||||
}
|
||||
|
||||
.withdrawal-item.failed {
|
||||
border-left: 4px solid #f56c6c;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-left .amount {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-left .chain {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pending-status {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.success-status {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.failed-status {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tx-hash {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
color: #409eff;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tx-hash i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 详情弹窗样式 */
|
||||
.detail-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 12px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.detail-value.amount {
|
||||
font-weight: 600;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.detail-value.address {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.address-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.address-container .detail-value {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.withdrawal-history-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-right {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.detail-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.detail-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.detail-content::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.detail-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
1288
power_leasing/src/views/cart/index.vue
Normal file
1288
power_leasing/src/views/cart/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
600
power_leasing/src/views/checkout/index.vue
Normal file
600
power_leasing/src/views/checkout/index.vue
Normal file
@@ -0,0 +1,600 @@
|
||||
<template>
|
||||
<div class="checkout-page">
|
||||
<h1 class="page-title">订单结算</h1>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
<el-loading-spinner></el-loading-spinner>
|
||||
加载中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="cartItems.length === 0" class="empty-cart">
|
||||
<div class="empty-icon">🛒</div>
|
||||
<h2>购物车是空的</h2>
|
||||
<p>请先添加商品到购物车</p>
|
||||
<router-link to="/productList" class="shop-now-btn">
|
||||
去购物
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-else class="checkout-content">
|
||||
<!-- 订单摘要 -->
|
||||
<div class="order-summary">
|
||||
<h2 class="section-title">订单摘要</h2>
|
||||
|
||||
<div class="order-items">
|
||||
<div
|
||||
v-for="item in cartItems"
|
||||
:key="item.id"
|
||||
class="order-item"
|
||||
>
|
||||
<div class="item-image">
|
||||
<img :src="item.image" :alt="item.title" />
|
||||
</div>
|
||||
|
||||
<div class="item-info">
|
||||
<h3 class="item-title">{{ item.title }}</h3>
|
||||
<div class="item-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
|
||||
<div class="item-quantity">
|
||||
<span class="quantity-label">数量:</span>
|
||||
<span class="quantity-value">{{ item.quantity }}</span>
|
||||
</div>
|
||||
|
||||
<div class="item-total">
|
||||
<span class="total-label">小计:</span>
|
||||
<span class="total-price">¥{{ (item.price * item.quantity).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-total">
|
||||
<div class="total-row">
|
||||
<span>商品总数:</span>
|
||||
<span>{{ summary.totalQuantity }} 件</span>
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span>商品种类:</span>
|
||||
<span>{{ cartItems.length }} 种</span>
|
||||
</div>
|
||||
<div class="total-row final-total">
|
||||
<span>订单总计:</span>
|
||||
<span class="final-amount">¥{{ summary.totalPrice.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结算表单 -->
|
||||
<div class="checkout-form">
|
||||
<h2 class="section-title">收货信息</h2>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">收货人姓名 *</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="form-input"
|
||||
required
|
||||
placeholder="请输入收货人姓名"
|
||||
aria-describedby="name-error"
|
||||
/>
|
||||
<div v-if="errors.name" id="name-error" class="error-message">
|
||||
{{ errors.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone" class="form-label">联系电话 *</label>
|
||||
<input
|
||||
id="phone"
|
||||
v-model="form.phone"
|
||||
type="tel"
|
||||
class="form-input"
|
||||
required
|
||||
placeholder="请输入联系电话"
|
||||
aria-describedby="phone-error"
|
||||
/>
|
||||
<div v-if="errors.phone" id="phone-error" class="error-message">
|
||||
{{ errors.phone }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="address" class="form-label">收货地址 *</label>
|
||||
<textarea
|
||||
id="address"
|
||||
v-model="form.address"
|
||||
class="form-textarea"
|
||||
rows="3"
|
||||
required
|
||||
placeholder="请输入详细收货地址"
|
||||
aria-describedby="address-error"
|
||||
></textarea>
|
||||
<div v-if="errors.address" id="address-error" class="error-message">
|
||||
{{ errors.address }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="note" class="form-label">备注</label>
|
||||
<textarea
|
||||
id="note"
|
||||
v-model="form.note"
|
||||
class="form-textarea"
|
||||
rows="2"
|
||||
placeholder="可选:订单备注信息"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<router-link to="/cart" class="back-btn">
|
||||
返回购物车
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="submit-btn"
|
||||
:disabled="submitting"
|
||||
aria-label="提交订单"
|
||||
>
|
||||
<span v-if="submitting">提交中...</span>
|
||||
<span v-else>提交订单</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { readCart, clearCart, computeSummary } from '../../utils/cartManager'
|
||||
|
||||
export default {
|
||||
name: 'Checkout',
|
||||
data() {
|
||||
return {
|
||||
cartItems: [],
|
||||
loading: false,
|
||||
submitting: false,
|
||||
form: {
|
||||
name: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
note: ''
|
||||
},
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
summary() {
|
||||
return computeSummary()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadCart()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载购物车数据
|
||||
*/
|
||||
loadCart() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.cartItems = readCart()
|
||||
|
||||
if (this.cartItems.length === 0) {
|
||||
this.$message.warning('购物车为空,请先添加商品')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载购物车失败:', error)
|
||||
console.log('加载购物车失败,请稍后重试')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证表单
|
||||
*/
|
||||
validateForm() {
|
||||
this.errors = {}
|
||||
|
||||
if (!this.form.name.trim()) {
|
||||
this.errors.name = '请输入收货人姓名'
|
||||
}
|
||||
|
||||
if (!this.form.phone.trim()) {
|
||||
this.errors.phone = '请输入联系电话'
|
||||
} else if (!/^1[3-9]\d{9}$/.test(this.form.phone.trim())) {
|
||||
this.errors.phone = '请输入正确的手机号码'
|
||||
}
|
||||
|
||||
if (!this.form.address.trim()) {
|
||||
this.errors.address = '请输入收货地址'
|
||||
}
|
||||
|
||||
return Object.keys(this.errors).length === 0
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理表单提交
|
||||
*/
|
||||
async handleSubmit() {
|
||||
if (!this.validateForm()) {
|
||||
this.$message.error('请完善收货信息')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.submitting = true
|
||||
|
||||
// 模拟订单提交
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// 构建订单数据
|
||||
const order = {
|
||||
id: `ORDER_${Date.now()}`,
|
||||
items: this.cartItems,
|
||||
total: this.summary.totalPrice,
|
||||
customer: {
|
||||
name: this.form.name,
|
||||
phone: this.form.phone,
|
||||
address: this.form.address,
|
||||
note: this.form.note
|
||||
},
|
||||
createTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
console.log('订单提交成功:', order)
|
||||
|
||||
// 清空购物车
|
||||
clearCart()
|
||||
|
||||
// 显示成功消息
|
||||
this.$message.success('订单提交成功!')
|
||||
|
||||
// 跳转到成功页面或商品列表
|
||||
setTimeout(() => {
|
||||
this.$router.push('/productList')
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交订单失败:', error)
|
||||
console.log('提交订单失败,请稍后重试')
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkout-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-cart {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-cart h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 12px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.empty-cart p {
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.shop-now-btn {
|
||||
display: inline-block;
|
||||
background: #42b983;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.shop-now-btn:hover {
|
||||
background: #3aa876;
|
||||
}
|
||||
|
||||
.checkout-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 20px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
/* 订单摘要样式 */
|
||||
.order-summary {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.order-items {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.order-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-image img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.item-quantity {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quantity-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.quantity-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.item-total {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.order-total {
|
||||
border-top: 2px solid #eee;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.final-total {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.final-amount {
|
||||
color: #e74c3c;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 结算表单样式 */
|
||||
.checkout-form {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #42b983;
|
||||
box-shadow: 0 0 0 3px rgba(66, 185, 131, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e74c3c;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 14px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s ease;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #42b983;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background: #3aa876;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.checkout-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checkout-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-image img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
580
power_leasing/src/views/productDetail/index.js
Normal file
580
power_leasing/src/views/productDetail/index.js
Normal file
@@ -0,0 +1,580 @@
|
||||
|
||||
import { getProductById } from '../../utils/productService'
|
||||
import { addToCart } from '../../utils/cartManager'
|
||||
import { getMachineInfo } from '../../api/products'
|
||||
import { addCart, getGoodsList } from '../../api/shoppingCart'
|
||||
|
||||
export default {
|
||||
name: 'ProductDetail',
|
||||
data() {
|
||||
return {
|
||||
product: null,
|
||||
loading: false,
|
||||
// 默认展开的行keys
|
||||
expandedRowKeys: [],
|
||||
selectedMap: {},
|
||||
params: {
|
||||
id: "",
|
||||
|
||||
|
||||
},
|
||||
confirmAddDialog: {
|
||||
visible: false,
|
||||
items: []
|
||||
},
|
||||
// 购物车中已存在的当前商品机器集合:id 与 user|miner 组合键
|
||||
cartMachineIdSet: new Set(),
|
||||
cartCompositeKeySet: new Set(),
|
||||
cartLoaded: false,
|
||||
machinesLoaded: false,
|
||||
/**
|
||||
* 可展开的产品系列数据
|
||||
* 每个系列(group)包含多个可选条目(variants)
|
||||
*/
|
||||
productListData: [
|
||||
// {
|
||||
// id: 'grp-1',
|
||||
// group: 'A系列',
|
||||
// summary: {
|
||||
// theoryPower: '56T',
|
||||
// computingPower: '54T',
|
||||
// powerDissipation: '3200W',
|
||||
// algorithm: 'power',
|
||||
// type: 'A-Pro',
|
||||
// count: 3,
|
||||
// price: '¥1000+'
|
||||
// },
|
||||
// variants: [
|
||||
// { id: 'A-1', model: 'A1', theoryPower: '14T', computingPower: '13.5T', powerDissipation: '780W', algorithm: 'power', stock: 50, price: 999, quantity: 1 },
|
||||
// { id: 'A-2', model: 'A2', theoryPower: '18T', computingPower: '17.2T', powerDissipation: '900W', algorithm: 'power', stock: 40, price: 1299, quantity: 1 },
|
||||
// { id: 'A-3', model: 'A3', theoryPower: '24T', computingPower: '23.1T', powerDissipation: '1520W', algorithm: 'power', stock: 30, price: 1699, quantity: 1 }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'grp-2',
|
||||
// group: 'B系列',
|
||||
// summary: {
|
||||
// theoryPower: '72T',
|
||||
// computingPower: '70T',
|
||||
// powerDissipation: '4100W',
|
||||
// algorithm: 'power',
|
||||
// type: 'B-Max',
|
||||
// count: 2,
|
||||
// price: '¥2000+'
|
||||
// },
|
||||
// variants: [
|
||||
// { id: 'B-1', model: 'B1', theoryPower: '32T', computingPower: '31.2T', powerDissipation: '1800W', algorithm: 'power', stock: 28, price: 2199, quantity: 1 },
|
||||
// { id: 'B-2', model: 'B2', theoryPower: '40T', computingPower: '38.8T', powerDissipation: '2300W', algorithm: 'power', stock: 18, price: 2699, quantity: 1 }
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
tableData: [
|
||||
// {
|
||||
// theoryPower: "55656",//理论算力
|
||||
// computingPower: "44545",//实际算力
|
||||
// powerDissipation: "5565",//功耗
|
||||
// algorithm: "power",//算法
|
||||
// type: "型号1",//矿机型号
|
||||
// number:2001,
|
||||
// cost:"1000",//价格
|
||||
// },
|
||||
// {
|
||||
// theoryPower: "55656",//理论算力
|
||||
// computingPower: "44545",//实际算力
|
||||
// powerDissipation: "5565",//功耗
|
||||
// algorithm: "power",//算法
|
||||
// type: "型号1",//矿机型号
|
||||
// number:2001,
|
||||
// cost:"1000",//价格
|
||||
// },
|
||||
// {
|
||||
// theoryPower: "55656",//理论算力
|
||||
// computingPower: "44545",//实际算力
|
||||
// powerDissipation: "5565",//功耗
|
||||
// algorithm: "power",//算法
|
||||
// type: "型号1",//矿机型号
|
||||
// number:2001,
|
||||
// cost:"1000",//价格
|
||||
// },
|
||||
// {
|
||||
// theoryPower: "55656",//理论算力
|
||||
// computingPower: "44545",//实际算力
|
||||
// powerDissipation: "5565",//功耗
|
||||
// algorithm: "power",//算法
|
||||
// type: "型号1",//矿机型号
|
||||
// number:2001,
|
||||
// cost:"1000",//价格
|
||||
// },
|
||||
|
||||
],
|
||||
productDetailLoading:false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.$route.params.id, "i叫哦附加费")
|
||||
if (this.$route.params.id) {
|
||||
this.params.id = this.$route.params.id
|
||||
this.product = true
|
||||
// 默认展开第一行
|
||||
if (this.productListData && this.productListData.length) {
|
||||
this.expandedRowKeys = [this.productListData[0].id]
|
||||
}
|
||||
this.fetchGetMachineInfo(this.params)
|
||||
} else {
|
||||
this.$message.error('商品不存在')
|
||||
this.product = false
|
||||
}
|
||||
this.fetchGetGoodsList()
|
||||
},
|
||||
methods: {
|
||||
|
||||
async fetchGetMachineInfo(params) {
|
||||
this.productDetailLoading = true
|
||||
const res = await getMachineInfo(params)
|
||||
console.log(res)
|
||||
if (res && res.code === 200) {
|
||||
console.log(res.data, 'res.rows');
|
||||
const list = Array.isArray(res.data) ? res.data : []
|
||||
const withKeys = list.map((group, idx) => {
|
||||
const fallbackId = `grp-${idx}`
|
||||
const groupId = group.id || group.onlyKey || (group.productMachineRangeGroupDto && group.productMachineRangeGroupDto.id)
|
||||
const firstMachineId = Array.isArray(group.productMachines) && group.productMachines.length > 0 ? group.productMachines[0].id : undefined
|
||||
// 为机器行设置默认租赁天数为1,并确保未选中状态
|
||||
const normalizedMachines = Array.isArray(group.productMachines)
|
||||
? group.productMachines.map(m => ({
|
||||
...m,
|
||||
leaseTime: (m && m.leaseTime && Number(m.leaseTime) > 0) ? Number(m.leaseTime) : 1,
|
||||
_selected: false // 确保所有机器行初始状态为未选中
|
||||
}))
|
||||
: []
|
||||
return { ...group, id: groupId || (firstMachineId ? `m-${firstMachineId}` : fallbackId), productMachines: normalizedMachines }
|
||||
})
|
||||
|
||||
this.productListData = withKeys
|
||||
if (this.productListData.length && (!this.expandedRowKeys || !this.expandedRowKeys.length)) {
|
||||
this.expandedRowKeys = [this.productListData[0].id]
|
||||
}
|
||||
// 产品机器加载完成后,依据购物车集合执行一次本地禁用与勾选
|
||||
this.$nextTick(() => {
|
||||
this.machinesLoaded = true
|
||||
// 已取消与购物车对比:不再自动禁用或勾选
|
||||
})
|
||||
}
|
||||
|
||||
this.productDetailLoading = false
|
||||
},
|
||||
/**
|
||||
* 加载商品详情
|
||||
*/
|
||||
async loadProduct() {
|
||||
try {
|
||||
this.loading = true
|
||||
const productId = this.$route.params.id
|
||||
this.product = await getProductById(productId)
|
||||
|
||||
if (!this.product) {
|
||||
this.$message({
|
||||
message: '商品不存在',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商品详情失败:', error)
|
||||
this.$message({
|
||||
message: '加载商品详情失败,请稍后重试',
|
||||
type: 'error',
|
||||
showClose: true
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
//加入购物车
|
||||
async fetchAddCart(params) {
|
||||
const res = await addCart(params)
|
||||
|
||||
return res
|
||||
},
|
||||
//查询购物车列表
|
||||
async fetchGetGoodsList(params) {
|
||||
const res = await getGoodsList(params)
|
||||
// 统计当前商品在购物车中已有的机器ID,用于禁用和默认勾选
|
||||
try {
|
||||
const productId = this.params && this.params.id ? Number(this.params.id) : Number(this.$route.params.id)
|
||||
// 兼容两种返回结构:1) 旧:直接是商品分组数组 2) 新:店铺数组 → shoppingCartInfoDtoList
|
||||
const rawRows = Array.isArray(res && res.rows)
|
||||
? res.rows
|
||||
: Array.isArray(res && res.data && res.data.rows)
|
||||
? res.data.rows
|
||||
: Array.isArray(res && res.data)
|
||||
? res.data
|
||||
: []
|
||||
// 扁平化为商品分组
|
||||
const groups = rawRows.length && rawRows[0] && Array.isArray(rawRows[0].shoppingCartInfoDtoList)
|
||||
? rawRows.flatMap(shop => Array.isArray(shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : [])
|
||||
: rawRows
|
||||
const matched = groups.filter(g => Number(g.productId) === productId)
|
||||
const ids = new Set()
|
||||
const compositeKeys = new Set()
|
||||
matched.forEach(r => {
|
||||
const list = Array.isArray(r.productMachineDtoList) ? r.productMachineDtoList : []
|
||||
list.forEach(m => {
|
||||
if (!m) return
|
||||
if (m.id !== undefined && m.id !== null) ids.add(String(m.id))
|
||||
if (m.user && m.miner) compositeKeys.add(`${String(m.user)}|${String(m.miner)}`)
|
||||
})
|
||||
})
|
||||
this.cartMachineIdSet = ids
|
||||
this.cartCompositeKeySet = compositeKeys
|
||||
// 计算购物车总数量并通知头部,避免页面初次加载时徽标显示为0
|
||||
try {
|
||||
const totalCount = groups.reduce((sum, g) => sum + (Array.isArray(g && g.productMachineDtoList) ? g.productMachineDtoList.length : 0), 0)
|
||||
if (Number.isFinite(totalCount)) {
|
||||
window.dispatchEvent(new CustomEvent('cart-updated', { detail: { count: totalCount } }))
|
||||
}
|
||||
} catch (e) { /* noop */ }
|
||||
// 展开表格渲染后,默认勾选并禁用这些行
|
||||
this.$nextTick(() => {
|
||||
this.cartLoaded = true
|
||||
this.autoSelectAndDisable()
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('解析购物车数据失败', e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理返回
|
||||
*/
|
||||
handleBack() {
|
||||
this.$router.push('/productList')
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 点击系列行:切换展开/收起
|
||||
* @param {Object} row - 当前行
|
||||
*/
|
||||
handleSeriesRowClick(row) {
|
||||
const key = row.id
|
||||
const lockedIds = Object.keys(this.selectedMap).filter(k => (this.selectedMap[k] || []).length > 0)
|
||||
const opened = this.expandedRowKeys.includes(key)
|
||||
if (opened) {
|
||||
// 关闭当前行,仅保留已勾选的行展开
|
||||
this.expandedRowKeys = lockedIds
|
||||
} else {
|
||||
// 打开当前行,同时保留已勾选的行展开
|
||||
this.expandedRowKeys = Array.from(new Set([key, ...lockedIds]))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 外层系列行样式
|
||||
*/
|
||||
handleGetSeriesRowClassName() {
|
||||
return 'series-clickable-row'
|
||||
},
|
||||
|
||||
// 子表选择变化
|
||||
handleInnerSelectionChange(parentRow, selections) {
|
||||
const key = parentRow.id
|
||||
this.$set(this.selectedMap, key, selections)
|
||||
const lockedIds = Object.keys(this.selectedMap).filter(k => (this.selectedMap[k] || []).length > 0)
|
||||
// 更新展开:锁定的行始终展开
|
||||
const openedSet = new Set(this.expandedRowKeys)
|
||||
lockedIds.forEach(id => openedSet.add(id))
|
||||
// 清理不再勾选且不是当前展开的行
|
||||
this.expandedRowKeys = Array.from(openedSet).filter(id => lockedIds.includes(id) || id === key || this.expandedRowKeys.includes(id))
|
||||
},
|
||||
|
||||
// 展开行变化时:已取消自动与购物车对比,无需勾选/禁用
|
||||
handleExpandChange(row, expandedRows) {
|
||||
// no-op
|
||||
},
|
||||
|
||||
// 已取消对比购物车的自动勾选/禁用逻辑
|
||||
autoSelectAndDisable() {},
|
||||
|
||||
// 选择器可选控制:已在购物车中的机器不可再选
|
||||
isSelectable(row, index) {
|
||||
// 不再通过 selectable 禁用,以便勾选可见;通过行样式和交互阻止点击
|
||||
return true
|
||||
},
|
||||
|
||||
// 判断在特定父行下是否已选择(配合自定义checkbox使用)
|
||||
isSelectedByParent(parentRow, row) {
|
||||
const key = parentRow && parentRow.id
|
||||
const list = (key && this.selectedMap[key]) || []
|
||||
return !!list.find(it => it && it.id === row.id)
|
||||
},
|
||||
|
||||
// 手动切换选择(自定义checkbox与 selectedMap 同步),并维护每行的 _selected 状态
|
||||
handleManualSelect(parentRow, row, checked) {
|
||||
const key = parentRow.id
|
||||
const list = (this.selectedMap[key] && [...this.selectedMap[key]]) || []
|
||||
const idx = list.findIndex(it => it && it.id === row.id)
|
||||
if (checked && idx === -1) list.push(row)
|
||||
if (!checked && idx > -1) list.splice(idx, 1)
|
||||
this.$set(this.selectedMap, key, list)
|
||||
this.$set(row, '_selected', !!checked)
|
||||
},
|
||||
|
||||
// 为子表中已在购物车的行添加只读样式,并阻止点击取消
|
||||
getInnerRowClass() {
|
||||
return ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 子行:减少数量
|
||||
* @param {number} groupIndex - 系列索引
|
||||
* @param {number} variantIndex - 变体索引
|
||||
*/
|
||||
handleDecreaseVariantQuantity(groupIndex, variantIndex) {
|
||||
const item = this.productListData[groupIndex].variants[variantIndex]
|
||||
if (item.quantity > 1) {
|
||||
item.quantity--
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 子行:增加数量
|
||||
* @param {number} groupIndex - 系列索引
|
||||
* @param {number} variantIndex - 变体索引
|
||||
*/
|
||||
handleIncreaseVariantQuantity(groupIndex, variantIndex) {
|
||||
const item = this.productListData[groupIndex].variants[variantIndex]
|
||||
if (item.quantity < 99) {
|
||||
item.quantity++
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 子行:输入数量校验
|
||||
* @param {number} groupIndex - 系列索引
|
||||
* @param {number} variantIndex - 变体索引
|
||||
*/
|
||||
handleVariantQuantityInput(groupIndex, variantIndex) {
|
||||
const item = this.productListData[groupIndex].variants[variantIndex]
|
||||
const q = Number(item.quantity)
|
||||
if (!q || q < 1) item.quantity = 1
|
||||
if (q > 99) item.quantity = 99
|
||||
},
|
||||
|
||||
/**
|
||||
* 子行:加入购物车
|
||||
* @param {Object} variant - 子项行数据
|
||||
*/
|
||||
handleAddVariantToCart(variant) {
|
||||
if (!variant || !variant.onlyKey) return
|
||||
try {
|
||||
addToCart({
|
||||
id: variant.onlyKey,
|
||||
title: variant.model,
|
||||
price: variant.price,
|
||||
quantity: variant.quantity
|
||||
})
|
||||
this.$message.success(`已添加 ${variant.quantity} 件 ${variant.model} 到购物车`)
|
||||
variant.quantity = 1
|
||||
} catch (error) {
|
||||
console.error('添加到购物车失败:', error)
|
||||
|
||||
}
|
||||
},
|
||||
// 统一加入购物车
|
||||
handleAddSelectedToCart() {
|
||||
const allSelected = Object.values(this.selectedMap).flat().filter(Boolean)
|
||||
if (!allSelected.length) {
|
||||
this.$message.warning('请先勾选至少一台矿机')
|
||||
return
|
||||
}
|
||||
try {
|
||||
allSelected.forEach(item => {
|
||||
addToCart({
|
||||
id: item.onlyKey || item.id,
|
||||
title: item.type || item.model || '矿机',
|
||||
price: item.price,
|
||||
quantity: 1,
|
||||
leaseTime: Number(item.leaseTime || 1)
|
||||
})
|
||||
})
|
||||
this.$message.success(`已加入 ${allSelected.length} 台矿机到购物车`)
|
||||
this.selectedMap = {}
|
||||
} catch (e) {
|
||||
console.error('统一加入购物车失败', e)
|
||||
|
||||
}
|
||||
},
|
||||
// 打开确认弹窗:以当前界面勾选(_selected)为准,并在打开后清空左侧勾选状态
|
||||
handleOpenAddToCartDialog() {
|
||||
// 扫描当前所有系列下被勾选的机器
|
||||
const groups = Array.isArray(this.productListData) ? this.productListData : []
|
||||
const picked = groups.flatMap(g => Array.isArray(g.productMachines) ? g.productMachines.filter(m => !!m && !!m._selected) : [])
|
||||
if (!picked.length) {
|
||||
this.$message.warning('请先勾选至少一台矿机')
|
||||
return
|
||||
}
|
||||
// 使用弹窗中的固定快照,避免后续清空勾选影响弹窗显示
|
||||
this.confirmAddDialog.items = picked.slice()
|
||||
this.confirmAddDialog.visible = true
|
||||
// 打开后立即把左侧复选框清空,避免“勾选了两个但弹窗只有一条”的不一致问题
|
||||
this.$nextTick(() => {
|
||||
try { this.clearAllSelections() } catch (e) { /* noop */ }
|
||||
})
|
||||
},
|
||||
// 确认加入:调用后端购物车接口,传入裸数组 [{ productId, productMachineId }]
|
||||
async handleConfirmAddSelectedToCart() {
|
||||
// 以弹窗中的列表为准,避免与左侧勾选状态不一致
|
||||
const allSelected = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
|
||||
if (!allSelected.length) {
|
||||
this.$message.warning('请先勾选至少一台矿机')
|
||||
return
|
||||
}
|
||||
|
||||
const productId = this.params && this.params.id ? this.params.id : (this.$route && this.$route.params && this.$route.params.id)
|
||||
if (!productId) {
|
||||
this.$message.error('商品ID缺失,无法加入购物车')
|
||||
return
|
||||
}
|
||||
|
||||
// 裸数组,仅包含后端要求的两个字段
|
||||
const payload = allSelected.map(item => ({
|
||||
productId: productId,
|
||||
productMachineId: item.id,
|
||||
leaseTime: Number(item.leaseTime || 1)
|
||||
}))
|
||||
|
||||
try {
|
||||
const res = await this.fetchAddCart(payload)
|
||||
// 若后端返回码存在,这里做一下兜底提示
|
||||
if (!res || (res.code && Number(res.code) !== 200)) {
|
||||
this.$message.error(res && res.msg ? res.msg : '加入购物车失败,请稍后重试')
|
||||
return
|
||||
}
|
||||
// 立即本地更新禁用状态:把刚加入的机器ID合并进本地集合
|
||||
try {
|
||||
allSelected.forEach(item => {
|
||||
if (item && item.id) this.cartMachineIdSet.add(item.id)
|
||||
this.$set(item, '_selected', false)
|
||||
this.$set(item, '_inCart', true)
|
||||
if (!item.leaseTime || Number(item.leaseTime) <= 0) this.$set(item, 'leaseTime', 1)
|
||||
})
|
||||
this.$nextTick(() => this.autoSelectAndDisable())
|
||||
} catch (e) { /* noop */ }
|
||||
|
||||
this.$message({
|
||||
message: `已加入 ${allSelected.length} 台矿机到购物车`,
|
||||
type: 'success',
|
||||
duration: 3000,
|
||||
showClose: true,
|
||||
});
|
||||
|
||||
this.confirmAddDialog.visible = false
|
||||
// 清空选中映射,然后重新加载数据(数据加载时会自动设置 _selected: false)
|
||||
this.selectedMap = {}
|
||||
// 重新加载机器信息和购物车数据
|
||||
this.fetchGetMachineInfo(this.params)
|
||||
this.fetchGetGoodsList()
|
||||
// 通知头部刷新服务端购物车数量
|
||||
try {
|
||||
// 如果没有传数量,header 会主动拉取服务端数量
|
||||
window.dispatchEvent(new CustomEvent('cart-updated'))
|
||||
} catch (e) { /* noop */ }
|
||||
|
||||
} catch (e) {
|
||||
console.error('加入购物车失败: ', e)
|
||||
this.$message.error('加入购物车失败,请稍后重试')
|
||||
}
|
||||
},
|
||||
|
||||
// 取消所有商品勾选(内层表格的自定义 checkbox)
|
||||
clearAllSelections() {
|
||||
try {
|
||||
// 清空选中映射
|
||||
this.selectedMap = {}
|
||||
// 遍历所有系列与机器,复位 _selected
|
||||
const groups = Array.isArray(this.productListData) ? this.productListData : []
|
||||
groups.forEach(g => {
|
||||
const list = Array.isArray(g.productMachines) ? g.productMachines : []
|
||||
list.forEach(m => { if (m) this.$set(m, '_selected', false) })
|
||||
})
|
||||
} catch (e) { /* noop */ }
|
||||
},
|
||||
|
||||
/**
|
||||
* 减少数量
|
||||
* @param {number} rowIndex - 表格行索引
|
||||
*/
|
||||
handleDecreaseQuantity(rowIndex) {
|
||||
if (this.tableData[rowIndex].quantity > 1) {
|
||||
this.tableData[rowIndex].quantity--
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 增加数量
|
||||
* @param {number} rowIndex - 表格行索引
|
||||
*/
|
||||
handleIncreaseQuantity(rowIndex) {
|
||||
if (this.tableData[rowIndex].quantity < 99) {
|
||||
this.tableData[rowIndex].quantity++
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理数量输入
|
||||
* @param {number} rowIndex - 表格行索引
|
||||
*/
|
||||
handleQuantityInput(rowIndex) {
|
||||
const quantity = this.tableData[rowIndex].quantity
|
||||
if (quantity < 1) {
|
||||
this.tableData[rowIndex].quantity = 1
|
||||
} else if (quantity > 99) {
|
||||
this.tableData[rowIndex].quantity = 99
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理数量输入框失焦
|
||||
* @param {number} rowIndex - 表格行索引
|
||||
*/
|
||||
handleQuantityBlur(rowIndex) {
|
||||
const quantity = this.tableData[rowIndex].quantity
|
||||
if (!quantity || quantity < 1) {
|
||||
this.tableData[rowIndex].quantity = 1
|
||||
} else if (quantity > 99) {
|
||||
this.tableData[rowIndex].quantity = 99
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加到购物车
|
||||
* @param {Object} rowData - 表格行数据
|
||||
*/
|
||||
handleAddToCart(rowData) {
|
||||
if (!rowData || rowData.quantity < 1) {
|
||||
this.$message.warning('请选择有效的数量')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
addToCart({
|
||||
id: rowData.date, // 使用矿机名称作为ID
|
||||
title: rowData.date,
|
||||
price: rowData.price,
|
||||
quantity: rowData.quantity,
|
||||
leaseTime: Number(rowData.leaseTime || 1)
|
||||
})
|
||||
|
||||
this.$message.success(`已添加 ${rowData.quantity} 件 ${rowData.date} 到购物车`)
|
||||
|
||||
// 重置数量
|
||||
rowData.quantity = 1
|
||||
} catch (error) {
|
||||
console.error('添加到购物车失败:', error)
|
||||
this.$message.error('添加到购物车失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
354
power_leasing/src/views/productDetail/index.vue
Normal file
354
power_leasing/src/views/productDetail/index.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="product-detail" v-loading="productDetailLoading">
|
||||
<div v-if="loading" class="loading">
|
||||
<i class="el-icon-loading" aria-label="加载中" role="img"></i>
|
||||
加载中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="product" class="detail-container">
|
||||
<h2 style="margin: 10px; text-align: left;margin-top: 28px;">商品详情-选择矿机</h2>
|
||||
<section class="productList">
|
||||
<!-- 产品列表(可展开) -->
|
||||
<el-table
|
||||
ref="seriesTable"
|
||||
:data="productListData"
|
||||
row-key="id"
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
@expand-change="handleExpandChange"
|
||||
@row-click="handleSeriesRowClick"
|
||||
:row-class-name="handleGetSeriesRowClassName"
|
||||
:header-cell-style="{ textAlign: 'left' }"
|
||||
:cell-style="{ textAlign: 'left' }"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="expand" width="46">
|
||||
<template #default="outer">
|
||||
<!-- 子表格:展开后显示该行的多个可选条目(来自 productMachines) -->
|
||||
<el-table :data="outer.row.productMachines" size="small" style="width: 100%" :show-header="true" :ref="'innerTable-' + outer.row.id" :row-key="'id'" :reserve-selection="false" :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
|
||||
<el-table-column width="46">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row._selected" @change="checked => handleManualSelect(outer.row, scope.row, checked)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 调整列顺序,使“理论算力”“实际算力”与上层对齐,并且指定相同宽度 -->
|
||||
<el-table-column prop="theoryPower" label="理论算力" width="280" header-align="left" align="left" />
|
||||
<el-table-column label="实际算力" width="230" header-align="left" align="left">
|
||||
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="powerDissipation" label="功耗(kw/h)" width="230" header-align="left" align="left" />
|
||||
<el-table-column prop="algorithm" label="算法" width="180" header-align="left" align="left" />
|
||||
|
||||
<el-table-column prop="theoryIncome" width="220" header-align="left" align="left">
|
||||
<template #header>单机理论收入(每日) <span v-show="outer.row.coin">({{ outer.row.coin || '' }})</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" width="240" header-align="left" align="left" />
|
||||
<!-- 矿机型号置于最后,不影响上层对齐 -->
|
||||
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" />
|
||||
<el-table-column label="租赁天数(天)" width="200" header-align="left" align="left">
|
||||
<template #default="scope">
|
||||
<el-input-number
|
||||
v-model="scope.row.leaseTime"
|
||||
:min="1"
|
||||
:max="365"
|
||||
size="mini"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 外层与内层列宽保持一致:160/160/160/120/160 -->
|
||||
<el-table-column label="价格" header-align="left" align="left">
|
||||
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.price }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="理论算力范围" width="280" header-align="left" align="left">
|
||||
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.theoryPowerRange }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际算力范围" width="230" header-align="left" align="left">
|
||||
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.computingPowerRange }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="功耗范围" width="230" header-align="left" align="left">
|
||||
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.powerRange }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="180" header-align="left" align="left">
|
||||
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.number }}</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<div style="margin: 18px; text-align: right;">
|
||||
<el-button type="primary" size="small" @click="handleOpenAddToCartDialog">加入购物车</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 确认加入购物车弹窗 -->
|
||||
<el-dialog :visible.sync="confirmAddDialog.visible" width="60vw" :title="`确认加入购物车(共 ${confirmAddDialog.items.length} 台)`">
|
||||
<div>
|
||||
<el-table :data="confirmAddDialog.items" height="360" border stripe :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
|
||||
<el-table-column prop="type" label="型号" width="160" header-align="left" align="left" />
|
||||
<el-table-column prop="theoryPower" label="理论算力" width="160" header-align="left" align="left" />
|
||||
<el-table-column label="算力" width="160" header-align="left" align="left">
|
||||
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="algorithm" label="算法" width="120" header-align="left" align="left" />
|
||||
<el-table-column prop="powerDissipation" label="功耗(kw/h)" width="160" header-align="left" align="left" />
|
||||
<el-table-column label="租赁天数(天)" width="160" header-align="left" align="left">
|
||||
<template #default="scope">{{ Number(scope.row.leaseTime || 1) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="price" label="单价(USDT)" width="160" header-align="left" align="left" />
|
||||
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="confirmAddDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmAddSelectedToCart">确认加入</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else class="not-found">
|
||||
<h2>商品不存在</h2>
|
||||
<p>抱歉,您查找的商品不存在或已被删除。</p>
|
||||
<button @click="handleBack" class="back-btn">返回商品列表</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Index from './index'
|
||||
export default {
|
||||
name: 'ProductDetail',
|
||||
mixins: [Index],
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-detail {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 子表内:已在购物车的行高亮并禁用指针事件到复选框外区域,保留复选框样式显示为勾选 */
|
||||
:deep(.in-cart-row) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 强制让被标记为 in-cart 的行的复选框始终为勾选视觉,并禁用鼠标交互 */
|
||||
:deep(.in-cart-row .el-checkbox.is-disabled .el-checkbox__inner) {
|
||||
background-color: #f5f7fa;
|
||||
border-color: #dcdfe6;
|
||||
}
|
||||
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.back-section {
|
||||
margin-bottom: 24px;
|
||||
text-align: left;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 40px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.product-image-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-info-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.product-price-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
/* 外层系列行:整行可点击的视觉提示 */
|
||||
:deep(.series-clickable-row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quantity-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.quantity-label {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
background: #f8f9fa;
|
||||
border: none;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.quantity-btn:hover:not(:disabled) {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.quantity-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 80px;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.quantity-input::-webkit-outer-spin-button,
|
||||
.quantity-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quantity-input[type=number] {
|
||||
/* 标准属性,隐藏默认的数字输入控件样式 */
|
||||
appearance: textfield;
|
||||
/* 浏览器前缀,兼容性处理 */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.quantity-input:focus {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.product-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.product-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.quantity-selector {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
311
power_leasing/src/views/productList/index.js
Normal file
311
power_leasing/src/views/productList/index.js
Normal file
@@ -0,0 +1,311 @@
|
||||
import { getProductList } from '../../api/products'
|
||||
export default {
|
||||
name: 'ProductList',
|
||||
data() {
|
||||
return {
|
||||
products: [
|
||||
// {
|
||||
// id: 1,
|
||||
// name: "Nexa",
|
||||
// price: `10000~20000`,
|
||||
// image: "https://img.yzcdn.cn/vant/apple-1.jpg",
|
||||
// desc: "NexaPow",
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// name: "grs",
|
||||
// price: `10000~20000`,
|
||||
// image: "https://img.yzcdn.cn/vant/apple-1.jpg",
|
||||
// desc: "groestl",
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// name: "mona",
|
||||
// price: `10000~20000`,
|
||||
// image: "https://img.yzcdn.cn/vant/apple-1.jpg",
|
||||
// desc: "Lyra2REv2",
|
||||
// },
|
||||
// {
|
||||
// id: 4,
|
||||
// name: "dgb",
|
||||
// price: `10000~20000`,
|
||||
// image: "https://img.yzcdn.cn/vant/apple-1.jpg",
|
||||
// desc: "DigiByte(Skein)",
|
||||
// },
|
||||
],
|
||||
loading: false,
|
||||
powerList: [
|
||||
{
|
||||
value: 1,
|
||||
label: "NexaPow",
|
||||
children: [
|
||||
{
|
||||
value: 1 - 1,
|
||||
label: "挖矿账户1",
|
||||
},
|
||||
{
|
||||
value: 1 - 2,
|
||||
label: "挖矿账户2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: "Grepow",
|
||||
children: [
|
||||
{
|
||||
value: 2 - 1,
|
||||
label: "挖矿账户1",
|
||||
},
|
||||
{
|
||||
value: 2 - 2,
|
||||
label: "挖矿账户2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: "mofang",
|
||||
children: [
|
||||
{
|
||||
value: 3 - 1,
|
||||
label: "挖矿账户1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
currencyList: [
|
||||
{
|
||||
path: "nexaAccess",
|
||||
value: "nexa",
|
||||
label: "nexa",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/nexa.png`,
|
||||
name: "course.NEXAcourse",
|
||||
show: true,
|
||||
amount: 10000,
|
||||
},
|
||||
{
|
||||
path: "grsAccess",
|
||||
value: "grs",
|
||||
label: "grs",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/grs.svg`,
|
||||
name: "course.GRScourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "monaAccess",
|
||||
value: "mona",
|
||||
label: "mona",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/mona.svg`,
|
||||
name: "course.MONAcourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "dgbsAccess",
|
||||
value: "dgbs",
|
||||
// label: "dgb-skein-pool1",
|
||||
label: "dgb(skein)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/dgb.svg`,
|
||||
name: "course.dgbsCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "dgbqAccess",
|
||||
value: "dgbq",
|
||||
// label: "dgb(qubit-pool1)",
|
||||
label: "dgb(qubit)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/dgb.svg`,
|
||||
name: "course.dgbqCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "dgboAccess",
|
||||
value: "dgbo",
|
||||
// label: "dgb-odocrypt-pool1",
|
||||
label: "dgb(odocrypt)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/dgb.svg`,
|
||||
name: "course.dgboCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
path: "rxdAccess",
|
||||
value: "rxd",
|
||||
label: "radiant(rxd)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/rxd.png`,
|
||||
name: "course.RXDcourse",
|
||||
show: true,
|
||||
amount: 100,
|
||||
},
|
||||
{
|
||||
path: "enxAccess",
|
||||
value: "enx",
|
||||
label: "Entropyx(enx)",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/enx.svg`,
|
||||
name: "course.ENXcourse",
|
||||
show: true,
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
path: "alphminingPool",
|
||||
value: "alph",
|
||||
label: "alephium",
|
||||
|
||||
imgUrl: `https://m2pool.com/img/alph.svg`,
|
||||
name: "course.alphCourse",
|
||||
show: true,
|
||||
amount: 1,
|
||||
},
|
||||
],
|
||||
screenCurrency: "",
|
||||
searchAlgorithm: "",
|
||||
params:{
|
||||
coin: "",
|
||||
algorithm: ""
|
||||
},
|
||||
productListLoading:false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchGetList()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 价格裁剪为两位小数(不四舍五入)
|
||||
* 兼容区间字符串:"min-max" 或 单值
|
||||
*/
|
||||
formatPriceRange(input) {
|
||||
try {
|
||||
if (input === null || input === undefined) return '0.00'
|
||||
const raw = String(input)
|
||||
if (raw.includes('-')) {
|
||||
const [lo, hi] = raw.split('-')
|
||||
return `${this._truncate2(lo)}-${this._truncate2(hi)}`
|
||||
}
|
||||
return this._truncate2(raw)
|
||||
} catch (e) {
|
||||
return '0.00'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 将任意数字字符串截断为 2 位小数(不四舍五入)。
|
||||
*/
|
||||
_truncate2(val) {
|
||||
if (val === null || val === undefined) return '0.00'
|
||||
const str = String(val).trim()
|
||||
if (!str) return '0.00'
|
||||
const [intPart, decPart = ''] = str.split('.')
|
||||
const two = decPart.slice(0, 2)
|
||||
return `${intPart}.${two.padEnd(2, '0')}`
|
||||
},
|
||||
handleCurrencyChange(val){
|
||||
try{
|
||||
// 清空时(el-select 的 clear 同时触发 change),避免重复请求,交由 handleCurrencyClear 处理
|
||||
if (val === undefined || val === null || val === '') return
|
||||
// 选择具体币种时,合并算法关键词一起查询
|
||||
this.params.coin = val
|
||||
const keyword = (this.searchAlgorithm || '').trim()
|
||||
const req = keyword ? { coin: val, algorithm: keyword } : { coin: val }
|
||||
this.fetchGetList(req)
|
||||
|
||||
|
||||
// 可在此发起接口:getProductList({ coin: val })
|
||||
// this.fetchGetList({ coin: val })
|
||||
}catch(e){
|
||||
console.error('处理币种变更失败', e)
|
||||
}
|
||||
},
|
||||
|
||||
async fetchGetList(params) {
|
||||
this.productListLoading = true
|
||||
try {
|
||||
const res = await getProductList(params)
|
||||
console.log('API响应:', res)
|
||||
if (res && res.code === 200) {
|
||||
this.products = res.rows || []
|
||||
console.log('商品数据:', this.products)
|
||||
} else {
|
||||
console.error('API返回错误:', res)
|
||||
this.products = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error)
|
||||
this.products = []
|
||||
// 添加一些测试数据,避免页面空白
|
||||
this.products = [
|
||||
// {
|
||||
// id: 1,
|
||||
// name: "测试商品1",
|
||||
// algorithm: "测试算法1",
|
||||
// priceRange: "100-200",
|
||||
// image: "https://img.yzcdn.cn/vant/apple-1.jpg"
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// name: "测试商品2",
|
||||
// algorithm: "测试算法2",
|
||||
// priceRange: "200-300",
|
||||
// image: "https://img.yzcdn.cn/vant/apple-1.jpg"
|
||||
// }
|
||||
]
|
||||
}
|
||||
this.productListLoading = false
|
||||
},
|
||||
// 算法搜索(使用同一接口,传入 algorithm 参数)
|
||||
handleAlgorithmSearch() {
|
||||
const keyword = (this.searchAlgorithm || '').trim()
|
||||
const next = { ...this.params }
|
||||
if (keyword) {
|
||||
next.algorithm = keyword
|
||||
this.params.algorithm = keyword
|
||||
} else {
|
||||
delete next.algorithm
|
||||
this.params.algorithm = ""
|
||||
}
|
||||
// 不重置下拉,只根据算法关键词查询
|
||||
if (next.algorithm) this.fetchGetList({ ...next, coin: this.screenCurrency || undefined })
|
||||
else this.fetchGetList(this.screenCurrency ? { coin: this.screenCurrency } : undefined)
|
||||
|
||||
},
|
||||
// 清空下拉时:只清 coin,保留算法条件
|
||||
handleCurrencyClear() {
|
||||
this.screenCurrency = ""
|
||||
this.params.coin = ""
|
||||
const keyword = (this.searchAlgorithm || '').trim()
|
||||
if (keyword) this.fetchGetList({ algorithm: keyword })
|
||||
else this.fetchGetList()
|
||||
},
|
||||
// 清空算法时:只清 algorithm,保留下拉 coin
|
||||
handleAlgorithmClear() {
|
||||
this.searchAlgorithm = ""
|
||||
this.params.algorithm = ""
|
||||
const coin = this.screenCurrency
|
||||
if (coin) this.fetchGetList({ coin })
|
||||
else this.fetchGetList()
|
||||
},
|
||||
handleProductClick(product) {
|
||||
|
||||
if (product.id || product.id == 0) {
|
||||
|
||||
this.$router.push(`/product/${product.id}`);
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
222
power_leasing/src/views/productList/index.vue
Normal file
222
power_leasing/src/views/productList/index.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="product-list" v-loading="productListLoading">
|
||||
<section class="container">
|
||||
<h1 class="page-title">商品列表</h1>
|
||||
<section class="filter-section">
|
||||
<label class="required" style="margin-bottom: 10px">币种选择:</label>
|
||||
<div class="filter-row">
|
||||
<!-- 币种下拉 -->
|
||||
<el-select
|
||||
class="input"
|
||||
size="middle"
|
||||
ref="screen"
|
||||
v-model="screenCurrency"
|
||||
placeholder="请选择"
|
||||
@change="handleCurrencyChange"
|
||||
@clear="handleCurrencyClear"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in currencyList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div style="display: flex; align-items: center">
|
||||
<img :src="item.imgUrl" style="float: left; width: 20px" />
|
||||
<span style="float: left; margin-left: 5px">{{ item.label }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 算法搜索框 -->
|
||||
<el-input
|
||||
v-model="searchAlgorithm"
|
||||
size="middle"
|
||||
placeholder="输入算法关键词"
|
||||
clearable
|
||||
@clear="handleAlgorithmClear"
|
||||
@keyup.enter.native="handleAlgorithmSearch"
|
||||
style="width: 240px;"
|
||||
>
|
||||
<template #append>
|
||||
<el-button type="primary" @click="handleAlgorithmSearch">搜索</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</section>
|
||||
<div class="product-list-grid">
|
||||
<div
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
class="product-item"
|
||||
@click="handleProductClick(product)"
|
||||
tabindex="0"
|
||||
aria-label="查看详情"
|
||||
>
|
||||
<img :src="product.image || 'https://img.yzcdn.cn/vant/apple-1.jpg'" :alt="product.name" class="product-image" />
|
||||
<div class="product-info">
|
||||
<h4>商品: {{ product.name }}</h4>
|
||||
<p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p>
|
||||
<div class="product-footer">
|
||||
|
||||
<span class="product-price">价格: {{ formatPriceRange(product.priceRange) }}</span> <span style="color: #999;font-size: 12px;">USDT</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="products.length === 0 && !productListLoading" class="empty-state">
|
||||
<i class="el-icon-goods"></i>
|
||||
<p>暂无商品数据</p>
|
||||
<p style="font-size: 12px; color: #999; margin-top: 8px;">请检查网络连接或联系管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listProducts } from "../../utils/productService";
|
||||
import { addToCart } from "../../utils/cartManager";
|
||||
import Index from "./index";
|
||||
|
||||
export default {
|
||||
mixins: [Index],
|
||||
name: "ProductList",
|
||||
|
||||
mounted() {},
|
||||
methods: {
|
||||
/**
|
||||
* 处理商品点击 - 跳转到详情页
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 处理添加到购物车
|
||||
*/
|
||||
handleAddToCart(product) {
|
||||
try {
|
||||
addToCart({
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
quantity: 1,
|
||||
});
|
||||
this.$message({
|
||||
message: "已添加到购物车",
|
||||
type: "success",
|
||||
showClose: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("添加到购物车失败:", error);
|
||||
console.log("添加到购物车失败,请稍后重试");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.product-list {
|
||||
background: #f5f5f5;
|
||||
padding: 24px;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
.filter-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
width: 80%;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.product-list-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 30px;
|
||||
margin-top: 100px;
|
||||
// background: palegoldenrod;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.product-item {
|
||||
width: 400px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 35vh;
|
||||
}
|
||||
.product-image {
|
||||
width: 90%;
|
||||
height:65%;
|
||||
object-fit: cover;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.product-info {
|
||||
width: 100%;
|
||||
}
|
||||
.product-footer {
|
||||
// display: flex;
|
||||
// justify-content: space-between;
|
||||
// align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.product-price {
|
||||
color: #e53e3e;
|
||||
font-weight: bold;
|
||||
max-width:90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.add-cart-btn {
|
||||
background: #42b983;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.add-cart-btn:hover {
|
||||
background: #369870;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
BIN
power_leasing/test.zip
Normal file
BIN
power_leasing/test.zip
Normal file
Binary file not shown.
1
power_leasing/test/css/app.1af80271.css
Normal file
1
power_leasing/test/css/app.1af80271.css
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/css/chunk-vendors.10dd4e95.css
Normal file
1
power_leasing/test/css/chunk-vendors.10dd4e95.css
Normal file
File diff suppressed because one or more lines are too long
BIN
power_leasing/test/favicon.ico
Normal file
BIN
power_leasing/test/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
power_leasing/test/fonts/element-icons.f1a45d74.ttf
Normal file
BIN
power_leasing/test/fonts/element-icons.f1a45d74.ttf
Normal file
Binary file not shown.
BIN
power_leasing/test/fonts/element-icons.ff18efd1.woff
Normal file
BIN
power_leasing/test/fonts/element-icons.ff18efd1.woff
Normal file
Binary file not shown.
1
power_leasing/test/index.html
Normal file
1
power_leasing/test/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>power_leasing</title><script defer="defer" src="/js/chunk-vendors.4487a7bc.js"></script><script defer="defer" src="/js/app.37d3fb13.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.1af80271.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but power_leasing doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
2
power_leasing/test/js/app.37d3fb13.js
Normal file
2
power_leasing/test/js/app.37d3fb13.js
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/js/app.37d3fb13.js.map
Normal file
1
power_leasing/test/js/app.37d3fb13.js.map
Normal file
File diff suppressed because one or more lines are too long
43
power_leasing/test/js/chunk-vendors.4487a7bc.js
Normal file
43
power_leasing/test/js/chunk-vendors.4487a7bc.js
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/js/chunk-vendors.4487a7bc.js.map
Normal file
1
power_leasing/test/js/chunk-vendors.4487a7bc.js.map
Normal file
File diff suppressed because one or more lines are too long
12
power_leasing/vue.config.js
Normal file
12
power_leasing/vue.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
client: {
|
||||
overlay: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
</header>
|
||||
|
||||
|
||||
<!-- ...头部已写好... -->
|
||||
|
||||
|
||||
<main>
|
||||
<!-- 1. Banner区 -->
|
||||
|
||||
Reference in New Issue
Block a user