chore: push latest changes

This commit is contained in:
wanwu
2025-11-16 22:13:57 +08:00
parent de821ae5fd
commit 7ede50739b
780 changed files with 101983 additions and 10460 deletions

View File

@@ -0,0 +1,182 @@
## 1. 产品概述
一款集短视频、长视频、音乐、小说于一体的综合娱乐平台应用,为用户提供多元化的数字娱乐体验。通过智能推荐算法,为用户推送个性化内容,打造一站式娱乐消费场景。
- 解决用户多平台切换的痛点,提供统一的娱乐内容消费体验
- 目标用户群体15-45岁的移动互联网用户涵盖娱乐内容消费者
- 通过内容生态整合,提升用户粘性和平台价值
## 2. 核心功能
### 2.1 用户角色
| 角色 | 注册方式 | 核心权限 |
|------|----------|----------|
| 游客用户 | 无需注册 | 浏览部分内容,基础播放功能 |
| 注册用户 | 手机号/第三方登录 | 完整播放权限,收藏评论,个人中心 |
| VIP会员 | 付费订阅 | 无广告观看,独家内容,高清播放 |
| 内容创作者 | 实名认证申请 | 发布内容,获得收益,数据分析 |
### 2.2 功能模块
本产品包含以下主要功能页面:
1. **首页**:个性化推荐内容聚合,分类导航入口
2. **短视频**:沉浸式竖屏短视频体验,支持滑动切换
3. **长视频**:电影、电视剧、综艺等长视频内容浏览
4. **音乐**:在线音乐播放,歌单管理,歌词同步
5. **小说**:电子书阅读,书架管理,分类浏览
6. **个人中心**:用户信息,观看历史,设置管理
7. **搜索**:全局内容搜索,智能联想
8. **播放页**:视频/音乐播放器,支持多种播放模式
9. **详情页**:内容详细介绍,相关推荐
10. **书架/收藏**:用户收藏的内容管理
### 2.3 页面详情
| 页面名称 | 模块名称 | 功能描述 |
|----------|----------|----------|
| 首页 | 推荐流 | 基于算法推荐个性化内容,支持下拉刷新 |
| 首页 | 分类导航 | 影视、音乐、小说等分类入口,图标展示 |
| 首页 | 搜索栏 | 顶部搜索框,支持语音输入和热门搜索 |
| 短视频 | 视频播放器 | 全屏竖屏播放,支持手势控制音量亮度 |
| 短视频 | 互动操作 | 点赞、评论、分享、关注创作者 |
| 短视频 | 视频流 | 上下滑动切换视频,预加载机制 |
| 长视频 | 分类筛选 | 按类型、地区、年份等维度筛选内容 |
| 长视频 | 排行榜 | 热播榜、新上架、评分榜等多维度排行 |
| 长视频 | 详情展示 | 剧集信息、演员表、剧情简介、评分 |
| 音乐 | 播放器 | 底部悬浮播放器,支持后台播放 |
| 音乐 | 歌单管理 | 创建、编辑、分享个人歌单 |
| 音乐 | 歌词显示 | 实时歌词同步,支持歌词翻译 |
| 小说 | 阅读器 | 仿真翻页效果,字体背景自定义 |
| 小说 | 书架管理 | 分类整理,阅读进度同步 |
| 小说 | 目录导航 | 章节列表,快速跳转,书签功能 |
| 个人中心 | 用户信息 | 头像、昵称、会员状态展示 |
| 个人中心 | 观看历史 | 跨设备同步,按时间分类展示 |
| 个人中心 | 设置管理 | 播放设置、通知设置、隐私设置 |
| 搜索 | 智能联想 | 输入时实时联想,热门搜索推荐 |
| 搜索 | 结果分类 | 按内容类型分类展示搜索结果 |
| 播放页 | 播放控制 | 播放/暂停、快进快退、倍速播放 |
| 播放页 | 画质选择 | 自动/高清/标清等多档画质切换 |
| 详情页 | 内容展示 | 详细介绍、演员信息、用户评分 |
| 详情页 | 相关推荐 | 基于内容相似度的推荐 |
## 3. 核心流程
### 3.1 用户浏览流程
用户打开App → 进入首页推荐流 → 选择感兴趣的内容 → 进入详情页 → 点击播放 → 观看内容 → 互动操作(点赞/评论/分享) → 返回继续浏览
### 3.2 短视频消费流程
进入短视频tab → 自动播放推荐视频 → 上下滑动切换 → 点赞评论互动 → 关注创作者 → 分享视频
### 3.3 长视频观看流程
浏览分类/搜索 → 选择影片 → 查看详情 → 选择剧集 → 播放观看 → 调整画质 → 添加收藏 → 继续观看其他内容
### 3.4 音乐播放流程
进入音乐tab → 浏览歌单/排行榜 → 选择歌曲 → 播放音乐 → 查看歌词 → 添加收藏 → 创建歌单
### 3.5 小说阅读流程
进入小说tab → 浏览分类/排行榜 → 选择小说 → 查看详情 → 开始阅读 → 调整设置 → 加入书架 → 继续阅读
```mermaid
graph TD
A[启动App] --> B{用户状态}
B -->|游客| C[浏览部分内容]
B -->|注册用户| D[完整体验]
B -->|VIP用户| E[无广告+独家内容]
C --> F[引导注册]
D --> G[个性化推荐]
E --> G
G --> H[底部导航]
H --> I[首页]
H --> J[短视频]
H --> K[长视频]
H --> L[音乐]
H --> M[小说]
H --> N[我的]
I --> O[内容消费]
J --> P[短视频播放]
K --> Q[长视频播放]
L --> R[音乐播放]
M --> S[小说阅读]
N --> T[个人管理]
```
## 4. 用户界面设计
### 4.1 设计风格
- **主色调**:深紫色渐变(#6B46C1#9333EA),营造高端娱乐氛围
- **辅助色**:暖橙色(#F97316)用于强调,深灰色(#1F2937)用于背景
- **按钮样式**圆角矩形3D悬浮效果点击有按压反馈
- **字体方案**:主标题使用思源黑体,正文使用苹方/思源黑体字号14-18px
- **布局风格**:卡片式布局,圆角设计,阴影效果,层次分明
- **图标风格**:线性图标为主,选中状态填充,符合现代设计趋势
- **动画效果**:页面切换使用淡入淡出,按钮点击有缩放效果,加载使用骨架屏
### 4.2 页面设计概述
| 页面名称 | 模块名称 | UI设计说明 |
|----------|----------|------------|
| 首页 | 顶部导航 | 渐变紫色背景,搜索框居中,右侧消息和个人头像 |
| 首页 | 推荐流 | 两列瀑布流布局卡片圆角8px阴影深度2px |
| 首页 | 分类导航 | 圆形图标+文字4×2网格布局图标使用线性风格 |
| 短视频 | 播放界面 | 全屏竖屏,底部操作栏半透明,右侧互动按钮垂直排列 |
| 短视频 | 用户资料 | 头像圆形边框,用户名白色,关注按钮橙色高亮 |
| 长视频 | 分类标签 | 横向滚动标签选中状态紫色背景圆角20px |
| 长视频 | 海报展示 | 16:9比例海报悬停效果评分标签右上角 |
| 音乐 | 播放器 | 底部悬浮条,专辑封面圆形,进度条渐变紫色 |
| 音乐 | 歌单列表 | 左侧封面+右侧信息,分割线浅灰色,悬停背景色 |
| 小说 | 阅读器 | 仿真纸张背景,护眼模式,字体大小可调节 |
| 小说 | 书架 | 网格布局书籍封面3D效果阅读进度条 |
| 个人中心 | 用户信息 | 顶部大图背景,头像圆形重叠,渐变遮罩 |
| 个人中心 | 功能列表 | 图标+文字+箭头,分组标题,分割线设计 |
| 搜索 | 搜索框 | 圆角输入框,语音图标,历史记录标签云 |
| 搜索 | 结果页 | Tab切换+卡片列表,加载更多按钮 |
### 4.3 响应式设计
- **移动端优先**基于375px宽度设计向上适配各种屏幕尺寸
- **平板适配**:横屏时采用双列布局,充分利用屏幕空间
- **手势优化**:支持滑动、捏合、长按等手势操作
- **横竖屏切换**:视频播放自动适配横竖屏,保持最佳观看体验
- **暗黑模式**:支持系统主题切换,护眼模式自动调节
- **字体缩放**:支持系统字体大小设置,保证可读性
### 4.4 交互体验
- **加载体验**:骨架屏预加载,减少等待焦虑
- **反馈机制**操作即时反馈toast提示震动反馈
- **导航体验**:底部导航固定,手势返回,面包屑导航
- **搜索体验**:实时联想,搜索历史,热门推荐
- **播放体验**:断点续播,后台播放,画中画模式
- **阅读体验**:仿真翻页,护眼模式,字体调节
## 5. 技术实现
### 5.1 开发框架
- **跨平台方案**uniapp-x一套代码多端运行
- **支持平台**iOS、Android、微信小程序、H5
- **状态管理**Vuex/Pinia统一管理应用状态
- **网络请求**uni.request封装支持拦截器
- **本地存储**uni.storage支持同步异步
### 5.2 性能优化
- **图片优化**懒加载WebP格式CDN加速
- **视频优化**:预加载策略,清晰度自适应,缓存机制
- **包体积优化**:按需加载,代码分割,资源压缩
- **内存优化**:页面销毁,资源释放,缓存清理
- **网络优化**:请求合并,缓存策略,弱网适配
### 5.3 用户体验
- **启动速度**:分包加载,预加载关键资源
- **播放流畅度**:多码率适配,缓冲策略优化
- **交互响应**:防抖节流,异步处理,骨架屏
- **离线体验**:内容缓存,离线阅读,断网提示
- **多端同步**:观看进度,收藏列表,阅读书签

View File

@@ -0,0 +1,549 @@
## 1. 架构设计
```mermaid
graph TD
A[用户设备] --> B[uniapp-x跨平台应用]
B --> C[微信小程序]
B --> D[iOS App]
B --> E[Android App]
B --> F[H5 Web]
C --> G[微信API]
D --> H[iOS原生API]
E --> I[Android原生API]
F --> J[浏览器API]
G --> K[业务服务层]
H --> K
I --> K
J --> K
K --> L[内容分发网络CDN]
K --> M[后端API服务]
K --> N[实时通信服务]
M --> O[(数据库)]
M --> P[(Redis缓存)]
M --> Q[(对象存储)]
subgraph "前端层"
B
C
D
E
F
end
subgraph "平台适配层"
G
H
I
J
end
subgraph "服务层"
K
L
M
N
O
P
Q
end
```
## 2. 技术描述
### 前端技术栈
* **跨平台框架**: uniapp-x (Vue3 + TypeScript)
* **状态管理**: Pinia (替代Vuex)
* **UI组件库**: uView-plus (uniapp生态组件库)
* **构建工具**: Vite (开发环境) + Webpack (生产环境)
* **样式方案**: SCSS + CSS变量 + Flex布局
* **图标方案**: iconfont + 本地SVG图标
* **动画库**: CSS3动画 + uni.createAnimation API
### 后端技术栈
* **API服务**: Node.js + Express/Koa2
* **数据库**: MySQL 8.0 (主数据库)
* **缓存**: Redis 6.0 (会话缓存 + 热点数据)
* **文件存储**: 阿里云OSS / 腾讯云COS
* **CDN加速**: 阿里云CDN / 腾讯云CDN
* **实时通信**: WebSocket + Socket.io
* **消息队列**: Redis Pub/Sub (轻量级)
### 第三方服务
* **视频服务**: 腾讯云点播 / 阿里云视频点播
* **音频服务**: 腾讯云音视频 / 网易云信
* **推送服务**: 个推 / 极光推送
* **登录认证**: 微信登录 + 手机号验证码
* **支付服务**: 微信支付 + 支付宝支付
## 3. 路由定义
### 底部导航路由
| 路由路径 | 页面名称 | 功能描述 |
| ------------------------------ | ---- | ------------- |
| /pages/index/index | 首页 | 推荐内容聚合,个性化内容流 |
| /pages/short-video/short-video | 短视频 | 沉浸式竖屏短视频播放 |
| /pages/long-video/long-video | 长视频 | 影视综等长视频内容分类 |
| /pages/music/music | 音乐 | 在线音乐播放和歌单管理 |
| /pages/novel/novel | 小说 | 电子书阅读和书架管理 |
| /pages/profile/profile | 我的 | 个人中心和相关设置 |
### 功能页面路由
| 路由路径 | 页面名称 | 功能描述 |
| -------------------------- | ---- | ----------- |
| /pages/search/search | 搜索页 | 全局内容搜索和智能联想 |
| /pages/player/player | 播放器 | 视频/音乐播放控制页面 |
| /pages/detail/detail | 详情页 | 内容详细信息展示 |
| /pages/category/category | 分类页 | 内容分类筛选和浏览 |
| /pages/reader/reader | 阅读器 | 小说阅读界面 |
| /pages/bookshelf/bookshelf | 书架 | 用户收藏的小说管理 |
| /pages/history/history | 历史记录 | 观看/收听/阅读历史 |
| /pages/settings/settings | 设置页 | 应用设置和偏好配置 |
| /pages/login/login | 登录页 | 用户登录和注册 |
| /pages/vip/vip | 会员页 | VIP会员开通和管理 |
### 子页面路由
| 路由路径 | 页面名称 | 功能描述 |
| --------------------------- | ---- | -------- |
| /pages/profile/edit-profile | 编辑资料 | 用户个人信息编辑 |
| /pages/profile/favorites | 我的收藏 | 收藏内容管理 |
| /pages/profile/download | 离线下载 | 离线内容管理 |
| /pages/music/playlist | 歌单详情 | 音乐歌单详细页面 |
| /pages/long-video/series | 剧集列表 | 电视剧分集选择 |
| /pages/short-video/upload | 上传视频 | 短视频上传发布 |
## 4. API定义
### 4.1 用户认证相关API
#### 用户登录
```
POST /api/auth/login
```
请求参数:
| 参数名 | 类型 | 必填 | 描述 |
| -------- | ------ | -- | -------------------------- |
| mobile | string | 是 | 手机号 |
| code | string | 是 | 短信验证码 |
| platform | string | 是 | 平台类型wechat/ios/android/h5 |
响应示例:
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"userInfo": {
"userId": "123456",
"nickname": "用户昵称",
"avatar": "https://example.com/avatar.jpg",
"vipLevel": 1,
"expireTime": "2024-12-31 23:59:59"
}
}
}
```
#### 获取验证码
```
POST /api/auth/sendCode
```
请求参数:
| 参数名 | 类型 | 必填 | 描述 |
| ------ | ------ | -- | -------------------- |
| mobile | string | 是 | 手机号 |
| type | string | 是 | 验证码类型login/register |
### 4.2 内容相关API
#### 获取首页推荐内容
```
GET /api/content/recommend
```
请求参数:
| 参数名 | 类型 | 必填 | 描述 |
| ----------- | ------ | -- | -------------------------- |
| page | number | 否 | 页码默认1 |
| pageSize | number | 否 | 每页数量默认20 |
| contentType | string | 否 | 内容类型all/video/music/novel |
#### 获取短视频列表
```
GET /api/short-video/list
```
请求参数:
| 参数名 | 类型 | 必填 | 描述 |
| -------- | ------ | -- | --------- |
| lastId | string | 否 | 最后一条视频ID |
| count | number | 否 | 获取数量默认10 |
| category | string | 否 | 视频分类 |
#### 获取长视频分类
```
GET /api/long-video/category
```
#### 获取音乐歌单
```
GET /api/music/playlist
```
#### 获取小说分类
```
GET /api/novel/category
```
### 4.3 播放相关API
#### 获取视频播放地址
```
GET /api/player/video/url
```
请求参数:
| 参数名 | 类型 | 必填 | 描述 |
| ------- | ------ | -- | ----------------- |
| videoId | string | 是 | 视频ID |
| quality | string | 否 | 清晰度auto/hd/sd/ld |
#### 获取音乐播放地址
```
GET /api/player/music/url
```
#### 记录播放进度
```
POST /api/player/progress
```
### 4.4 用户行为API
#### 点赞/取消点赞
```
POST /api/interact/like
```
#### 收藏/取消收藏
```
POST /api/interact/favorite
```
#### 发表评论
```
POST /api/interact/comment
```
#### 关注/取消关注
```
POST /api/interact/follow
```
### 4.5 搜索相关API
#### 搜索建议
```
GET /api/search/suggest
```
#### 搜索结果
```
GET /api/search/result
```
## 5. 服务器架构设计
```mermaid
graph TD
A[客户端请求] --> B[API网关层]
B --> C[负载均衡器]
C --> D[应用服务集群]
D --> E[用户服务]
D --> F[内容服务]
D --> G[播放服务]
D --> H[推荐服务]
E --> I[(用户数据库)]
F --> J[(内容数据库)]
G --> K[(播放记录)]
H --> L[(Redis缓存)]
D --> M[消息队列]
M --> N[日志服务]
M --> O[统计服务]
subgraph "网关层"
B
C
end
subgraph "应用层"
D
E
F
G
H
end
subgraph "数据层"
I
J
K
L
end
subgraph "服务层"
M
N
O
end
```
## 6. 数据模型
### 6.1 用户相关数据模型
```mermaid
erDiagram
USER ||--o{ USER_PROFILE : has
USER ||--o{ USER_VIP : has
USER ||--o{ USER_FAVORITE : has
USER ||--o{ USER_HISTORY : has
USER ||--o{ USER_FOLLOW : follows
USER {
string userId PK
string mobile UK
string password
string nickname
string avatar
integer status
datetime createTime
datetime updateTime
}
USER_PROFILE {
string userId PK
string gender
date birthday
string city
string signature
integer level
integer experience
}
USER_VIP {
string userId PK
integer vipLevel
datetime startTime
datetime expireTime
boolean autoRenew
}
USER_FAVORITE {
string id PK
string userId FK
string contentId FK
string contentType
datetime createTime
}
USER_HISTORY {
string id PK
string userId FK
string contentId FK
string contentType
integer progress
datetime lastPlayTime
}
```
### 6.2 内容相关数据模型
```mermaid
erDiagram
CONTENT ||--o{ CONTENT_DETAIL : has
CONTENT ||--o{ CONTENT_TAG : has
CONTENT ||--o{ CONTENT_STAT : has
CONTENT ||--o{ COMMENT : has
CONTENT {
string contentId PK
string title
string cover
string contentType
string category
integer duration
string tags
integer status
datetime publishTime
}
CONTENT_DETAIL {
string contentId PK
string description
string director
string actors
string area
integer year
float rating
integer episodeCount
}
CONTENT_TAG {
string id PK
string contentId FK
string tagName
integer weight
}
CONTENT_STAT {
string contentId PK
integer viewCount
integer likeCount
integer favoriteCount
integer commentCount
integer shareCount
}
```
### 6.3 数据库表结构示例
#### 用户表 (users)
```sql
CREATE TABLE users (
user_id VARCHAR(32) PRIMARY KEY COMMENT '用户ID',
mobile VARCHAR(11) UNIQUE NOT NULL COMMENT '手机号',
password VARCHAR(64) NOT NULL COMMENT '密码',
nickname VARCHAR(50) NOT NULL COMMENT '昵称',
avatar VARCHAR(255) COMMENT '头像URL',
status TINYINT DEFAULT 1 COMMENT '状态1正常 0禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_mobile (mobile),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
```
#### 内容表 (contents)
```sql
CREATE TABLE contents (
content_id VARCHAR(32) PRIMARY KEY COMMENT '内容ID',
title VARCHAR(100) NOT NULL COMMENT '标题',
cover VARCHAR(255) COMMENT '封面URL',
content_type VARCHAR(20) NOT NULL COMMENT '内容类型video/music/novel',
category VARCHAR(50) COMMENT '分类',
duration INT COMMENT '时长(秒)',
tags TEXT COMMENT '标签,逗号分隔',
status TINYINT DEFAULT 1 COMMENT '状态1正常 0下架',
publish_time DATETIME COMMENT '发布时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_type_category (content_type, category),
INDEX idx_status_time (status, publish_time DESC),
INDEX idx_tags (tags(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='内容表';
```
#### 用户历史记录表 (user\_history)
```sql
CREATE TABLE user_history (
id VARCHAR(32) PRIMARY KEY COMMENT '记录ID',
user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
content_id VARCHAR(32) NOT NULL COMMENT '内容ID',
content_type VARCHAR(20) NOT NULL COMMENT '内容类型',
progress INT DEFAULT 0 COMMENT '播放进度(秒)',
last_play_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '最后播放时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY uk_user_content (user_id, content_id),
INDEX idx_user_time (user_id, last_play_time DESC),
INDEX idx_content_type (content_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户播放历史表';
```
## 7. 性能优化方案
### 7.1 前端优化
* **资源压缩**:图片压缩、代码混淆、分包加载
* **懒加载**:图片懒加载、组件懒加载、路由懒加载
* **缓存策略**:接口缓存、图片缓存、离线包机制
* **渲染优化**:虚拟列表、防抖节流、异步渲染
### 7.2 后端优化
* **数据库优化**:索引优化、查询优化、分库分表
* **缓存策略**Redis缓存、CDN缓存、浏览器缓存
* **接口优化**:接口合并、数据压缩、分页加载
* **并发处理**:连接池、异步处理、队列机制
### 7.3 内容分发优化
* **CDN加速**静态资源CDN、视频CDN、音频CDN
* **预加载策略**:智能预加载、按需加载、优先级加载
* **多码率适配**:根据网络自动选择清晰度
* **断点续传**:支持大文件断点续传,提升用户体验

View File

@@ -93,6 +93,41 @@ export const getCustomMenuApi = () => {
return request.get('/adminapi/wechat/menu'); return request.get('/adminapi/wechat/menu');
}; };
// 获取菜单列表
export const getMenuListApi = (params: { page?: number; limit?: number }) => {
return request.get('/adminapi/wechat/menu', { params });
};
// 获取菜单详情
export const getWechatMenuInfo = (id: number) => {
return request.get(`/adminapi/wechat/menu/${id}`);
};
// 创建菜单
export const createWechatMenu = (data: any) => {
return request.post('/adminapi/wechat/menu', data);
};
// 更新菜单
export const updateWechatMenu = (data: any) => {
return request.put('/adminapi/wechat/menu', data);
};
// 删除菜单
export const deleteWechatMenu = (id: number) => {
return request.delete(`/adminapi/wechat/menu/${id}`);
};
// 同步菜单
export const syncWechatMenu = () => {
return request.post('/adminapi/wechat/menu/sync');
};
// 发布菜单
export const publishWechatMenu = () => {
return request.post('/adminapi/wechat/menu/publish');
};
// 保存自定义菜单 // 保存自定义菜单
export const saveCustomMenuApi = (data: { export const saveCustomMenuApi = (data: {
button: Array<{ button: Array<{
@@ -147,10 +182,29 @@ export const getUserListApi = (params: { page?: number; limit?: number; nickname
}; };
// 同步用户 // 同步用户
export const syncUserApi = () => { export const syncWechatUser = () => {
return request.post('/adminapi/wechat/user/sync'); return request.post('/adminapi/wechat/user/sync');
}; };
// 导出用户
export const exportWechatUser = () => {
return request.post('/adminapi/wechat/user/export');
};
// 更新用户信息
export const updateWechatUser = (data: {
openid: string;
remark?: string;
groupid?: number;
}) => {
return request.put('/adminapi/wechat/user', data);
};
// 获取用户信息
export const getWechatUserInfo = (openid: string) => {
return request.get(`/adminapi/wechat/user/${openid}`);
};
// 获取用户详情 // 获取用户详情
export const getUserDetailApi = (openid: string) => { export const getUserDetailApi = (openid: string) => {
return request.get(`/adminapi/wechat/user/${openid}`); return request.get(`/adminapi/wechat/user/${openid}`);
@@ -161,16 +215,26 @@ export const getMaterialListApi = (params: { page?: number; limit?: number; type
return request.get('/adminapi/wechat/material', { params }); return request.get('/adminapi/wechat/material', { params });
}; };
// 获取素材详情
export const getWechatMaterialInfo = (id: number) => {
return request.get(`/adminapi/wechat/material/${id}`);
};
// 同步素材 // 同步素材
export const syncMaterialApi = () => { export const syncWechatMaterial = () => {
return request.post('/adminapi/wechat/material/sync'); return request.post('/adminapi/wechat/material/sync');
}; };
// 上传素材 // 上传素材
export const uploadMaterialApi = (data: FormData) => { export const uploadWechatMaterial = (data: FormData) => {
return request.post('/adminapi/wechat/material/upload', data); return request.post('/adminapi/wechat/material/upload', data);
}; };
// 更新素材
export const updateWechatMaterial = (data: any) => {
return request.put('/adminapi/wechat/material', data);
};
// 删除素材 // 删除素材
export const deleteMaterialApi = (id: number) => { export const deleteMaterialApi = (id: number) => {
return request.delete(`/adminapi/wechat/material/${id}`); return request.delete(`/adminapi/wechat/material/${id}`);

View File

@@ -18,6 +18,45 @@
"appList": "应用列表", "appList": "应用列表",
"chooseLayout": "选择布局" "chooseLayout": "选择布局"
}, },
"menu": {
"auth": "权限管理",
"user": "用户管理",
"role": "角色管理",
"menu": "菜单管理",
"site": "站点管理",
"siteGroup": "站点分组",
"diy": "DIY装修",
"channel": {
"weapp": "微信小程序",
"wechat": {
"access": "接入指引",
"config": "配置管理",
"template": "模板消息",
"menu": "自定义菜单",
"user": "用户管理",
"material": "素材管理",
"tutorial": "使用教程"
}
},
"setting": {
"system": "系统设置",
"payment": "支付设置",
"sms": "短信设置",
"storage": "存储设置"
},
"app": {
"list": "应用管理"
},
"tools": {
"backup": "数据备份"
},
"finance": {
"payment": "支付记录"
},
"log": {
"admin": "管理员日志"
}
},
"channel": { "channel": {
"weapp": { "weapp": {
"title": "微信小程序", "title": "微信小程序",
@@ -56,4 +95,4 @@
"list": "存储配置" "list": "存储配置"
} }
} }
} }

View File

@@ -0,0 +1,218 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '@vben/locale';
const routes: RouteRecordRaw[] = [
{
path: '/auth',
name: 'Auth',
component: () => import('#/views/auth/list.vue'),
meta: {
title: $t('menu.auth'),
icon: 'mdi:account-key',
permissions: ['auth.manage'],
},
},
{
path: '/user',
name: 'User',
component: () => import('#/views/user/list.vue'),
meta: {
title: $t('menu.user'),
icon: 'mdi:account-group',
permissions: ['user.manage'],
},
},
{
path: '/role',
name: 'Role',
component: () => import('#/views/role/list.vue'),
meta: {
title: $t('menu.role'),
icon: 'mdi:shield-account',
permissions: ['role.manage'],
},
},
{
path: '/menu',
name: 'Menu',
component: () => import('#/views/menu/list.vue'),
meta: {
title: $t('menu.menu'),
icon: 'mdi:menu',
permissions: ['menu.manage'],
},
},
{
path: '/site',
name: 'Site',
component: () => import('#/views/site/list.vue'),
meta: {
title: $t('menu.site'),
icon: 'mdi:web',
permissions: ['site.manage'],
},
},
{
path: '/site-group',
name: 'SiteGroup',
component: () => import('#/views/site/group.vue'),
meta: {
title: $t('menu.siteGroup'),
icon: 'mdi:folder-multiple',
permissions: ['site.group.manage'],
},
},
{
path: '/diy',
name: 'Diy',
component: () => import('#/views/diy/list.vue'),
meta: {
title: $t('menu.diy'),
icon: 'mdi:palette',
permissions: ['diy.manage'],
},
},
{
path: '/channel/weapp',
name: 'ChannelWeapp',
component: () => import('#/views/channel/weapp/list.vue'),
meta: {
title: $t('menu.channel.weapp'),
icon: 'mdi:wechat',
permissions: ['channel.weapp.manage'],
},
},
{
path: '/channel/wechat/access',
name: 'ChannelWechatAccess',
component: () => import('#/views/channel/wechat/access/list.vue'),
meta: {
title: $t('menu.channel.wechat.access'),
icon: 'mdi:account-check',
permissions: ['channel.wechat.manage'],
},
},
{
path: '/channel/wechat/config',
name: 'ChannelWechatConfig',
component: () => import('#/views/channel/wechat/config/list.vue'),
meta: {
title: $t('menu.channel.wechat.config'),
icon: 'mdi:cog',
permissions: ['channel.wechat.manage'],
},
},
{
path: '/channel/wechat/template',
name: 'ChannelWechatTemplate',
component: () => import('#/views/channel/wechat/template/list.vue'),
meta: {
title: $t('menu.channel.wechat.template'),
icon: 'mdi:file-document',
permissions: ['channel.wechat.manage'],
},
},
{
path: '/channel/wechat/version',
name: 'ChannelWechatVersion',
component: () => import('#/views/channel/wechat/version/list.vue'),
meta: {
title: $t('menu.channel.wechat.version'),
icon: 'mdi:tag',
permissions: ['channel.wechat.manage'],
},
},
{
path: '/channel/wechat/tutorial',
name: 'ChannelWechatTutorial',
component: () => import('#/views/channel/wechat/tutorial/list.vue'),
meta: {
title: $t('menu.channel.wechat.tutorial'),
icon: 'mdi:book',
permissions: ['channel.wechat.manage'],
},
},
{
path: '/channel/wechat/menu',
name: 'ChannelWechatMenu',
component: () => import('#/views/channel/wechat/menu/list.vue'),
meta: {
title: $t('menu.channel.wechat.menu'),
icon: 'mdi:menu',
permissions: ['channel.wechat.menu.manage'],
},
},
{
path: '/channel/wechat/user',
name: 'ChannelWechatUser',
component: () => import('#/views/channel/wechat/user/list.vue'),
meta: {
title: $t('menu.channel.wechat.user'),
icon: 'mdi:account-multiple',
permissions: ['channel.wechat.user.manage'],
},
},
{
path: '/channel/wechat/material',
name: 'ChannelWechatMaterial',
component: () => import('#/views/channel/wechat/material/list.vue'),
meta: {
title: $t('menu.channel.wechat.material'),
icon: 'mdi:folder-image',
permissions: ['channel.wechat.material.manage'],
},
},
{
path: '/setting/system',
name: 'SettingSystem',
component: () => import('#/views/setting/system/list.vue'),
meta: {
title: $t('menu.setting.system'),
icon: 'mdi:cog',
permissions: ['setting.system.manage'],
},
},
{
path: '/app/list',
name: 'AppList',
component: () => import('#/views/app/list/list.vue'),
meta: {
title: $t('menu.app.list'),
icon: 'mdi:apps',
permissions: ['app.manage'],
},
},
{
path: '/tools/backup',
name: 'ToolsBackup',
component: () => import('#/views/tools/backup/list.vue'),
meta: {
title: $t('menu.tools.backup'),
icon: 'mdi:backup',
permissions: ['tools.backup.manage'],
},
},
{
path: '/finance/payment',
name: 'FinancePayment',
component: () => import('#/views/finance/payment/list.vue'),
meta: {
title: $t('menu.finance.payment'),
icon: 'mdi:cash-multiple',
permissions: ['finance.payment.manage'],
},
},
{
path: '/log/admin',
name: 'LogAdmin',
component: () => import('#/views/log/admin/list.vue'),
meta: {
title: $t('menu.log.admin'),
icon: 'mdi:file-document',
permissions: ['log.admin.manage'],
},
},
];
export default routes;

View File

@@ -0,0 +1,135 @@
import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface AppInfo {
id: number;
name: string;
title: string;
description: string;
author: string;
version: string;
icon: string;
cover: string;
preview: string;
path: string;
admin_path: string;
type: 'addon' | 'module' | 'plugin';
category: string;
tags: string;
require: string;
install: 0 | 1;
status: 0 | 1;
config: string;
hooks: string;
create_time: string;
update_time: string;
}
export interface AppForm {
id?: number;
name: string;
title: string;
description: string;
author: string;
version: string;
icon: string;
cover: string;
preview: string;
path: string;
admin_path: string;
type: string;
category: string;
tags: string;
require: string;
install: 0 | 1;
status: 0 | 1;
config: string;
hooks: string;
}
export const typeOptions = [
{ label: '插件', value: 'addon' },
{ label: '模块', value: 'module' },
{ label: '应用', value: 'plugin' },
];
export const categoryOptions = [
{ label: '系统工具', value: 'system' },
{ label: '营销工具', value: 'marketing' },
{ label: '支付工具', value: 'payment' },
{ label: '物流工具', value: 'logistics' },
{ label: '客服工具', value: 'service' },
{ label: '数据分析', value: 'analytics' },
{ label: '其他', value: 'other' },
];
export const statusOptions = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
];
export const installOptions = [
{ label: '已安装', value: 1 },
{ label: '未安装', value: 0 },
];
export const gridOptions: VxeGridProps<AppInfo> = {
columns: [
{ type: 'checkbox', width: 50 },
{ field: 'icon', title: '图标', width: 80, formatter: ({ cellValue }) => {
return cellValue ? `<i class="${cellValue}" style="font-size: 24px;"></i>` : '';
} },
{ field: 'title', title: '应用名称', minWidth: 150 },
{ field: 'name', title: '应用标识', minWidth: 120 },
{ field: 'version', title: '版本', width: 100 },
{ field: 'author', title: '作者', width: 120 },
{ field: 'type', title: '类型', width: 100, formatter: ({ cellValue }) => {
const option = typeOptions.find(item => item.value === cellValue);
return option?.label || cellValue;
}},
{ field: 'category', title: '分类', width: 100, formatter: ({ cellValue }) => {
const option = categoryOptions.find(item => item.value === cellValue);
return option?.label || cellValue;
}},
{ field: 'install', title: '安装状态', width: 100, formatter: ({ cellValue }) => {
return cellValue === 1 ? '已安装' : '未安装';
}},
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
return cellValue === 1 ? '启用' : '禁用';
}},
{ field: 'create_time', title: '创建时间', width: 180 },
{
field: 'action',
fixed: 'right',
title: '操作',
width: 200,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: AppInfo) => {
// This will be handled in the component
},
options: [
{ code: 'install', text: '安装', icon: 'ant-design:download-outlined' },
{ code: 'config', text: '配置', icon: 'ant-design:setting-outlined' },
{ code: 'uninstall', text: '卸载', icon: 'ant-design:delete-outlined', danger: true },
],
},
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
print: true,
refresh: true,
zoom: true,
},
};

View File

@@ -0,0 +1,269 @@
<template>
<Page auto-content-height>
<VbenVxeGrid
ref="gridRef"
:grid-options="gridOptions"
:query-form-schema="queryFormSchema"
@toolbar-button-click="handleToolbarButtonClick"
>
<template #toolbar-tools>
<VbenButton type="primary" @click="handleInstallFromStore">
<SvgIcon icon="mdi:download" class="mr-1" />
应用商店
</VbenButton>
</template>
<template #icon="{ row }">
<img
v-if="row.icon"
:src="row.icon"
alt="应用图标"
class="w-10 h-10 rounded object-cover"
@error="(e: any) => e.target.src = 'https://via.placeholder.com/40x40'"
/>
<div v-else class="w-10 h-10 bg-gray-200 rounded flex items-center justify-center">
<SvgIcon icon="mdi:application" class="text-gray-400" />
</div>
</template>
<template #action="{ row }">
<VbenButton
v-if="row.install === 0"
size="small"
type="primary"
variant="text"
@click="handleInstall(row)"
>
安装
</VbenButton>
<template v-else>
<VbenButton
size="small"
type="primary"
variant="text"
@click="handleConfig(row)"
>
配置
</VbenButton>
<VbenButton
size="small"
type="primary"
variant="text"
@click="handleEdit(row)"
>
{{ $t('common.edit') }}
</VbenButton>
<VbenButton
size="small"
type="warning"
variant="text"
@click="handleUpdate(row)"
>
更新
</VbenButton>
<VbenPopconfirm
title="确定卸载该应用吗?"
@confirm="handleUninstall(row)"
>
<VbenButton
size="small"
type="danger"
variant="text"
>
卸载
</VbenButton>
</VbenPopconfirm>
</template>
</template>
</VbenVxeGrid>
<AppFormModal
v-model:visible="modalVisible"
:id="editingId"
@cancel="handleModalCancel"
@submit="handleModalSubmit"
/>
</Page>
</template>
<script lang="ts" setup>
import type { AppInfo, AppForm } from './data';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenVxeGrid } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { getAppListApi, installAppApi, uninstallAppApi, updateAppApi, getAppStoreListApi } from '#/api/core/app';
import { SvgIcon } from '#/components/icon';
import AppFormModal from './modules/form.vue';
import { gridOptions } from './data';
const router = useRouter();
const gridRef = ref();
const modalVisible = ref(false);
const editingId = ref<number | undefined>();
const queryFormSchema = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '应用标识',
},
{
component: 'Input',
fieldName: 'title',
label: '应用名称',
},
{
component: 'Select',
fieldName: 'type',
label: '应用类型',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '插件', value: 'addon' },
{ label: '模块', value: 'module' },
{ label: '应用', value: 'plugin' },
],
placeholder: '请选择应用类型',
},
},
{
component: 'Select',
fieldName: 'install',
label: '安装状态',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '已安装', value: 1 },
{ label: '未安装', value: 0 },
],
placeholder: '请选择安装状态',
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
placeholder: '请选择状态',
},
},
]);
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
queryFormSchema,
});
function handleToolbarButtonClick(event: string) {
switch (event) {
case 'add':
handleAdd();
break;
case 'refresh':
handleRefresh();
break;
case 'export':
handleExport();
break;
default:
break;
}
}
function handleAdd() {
editingId.value = undefined;
modalVisible.value = true;
}
function handleInstallFromStore() {
// Navigate to app store
router.push({ name: 'AppStore' });
}
function handleConfig(row: AppInfo) {
// Navigate to app config page
router.push({
name: 'AppConfig',
params: { id: row.id },
query: { name: row.name }
});
}
function handleEdit(row: AppInfo) {
editingId.value = row.id;
modalVisible.value = true;
}
async function handleInstall(row: AppInfo) {
try {
await installAppApi(row.id);
await handleRefresh();
$message.success('安装成功');
} catch (error) {
$message.error('安装失败');
}
}
async function handleUninstall(row: AppInfo) {
try {
await uninstallAppApi(row.id);
await handleRefresh();
$message.success('卸载成功');
} catch (error) {
$message.error('卸载失败');
}
}
async function handleUpdate(row: AppInfo) {
try {
await updateAppApi(row.id);
await handleRefresh();
$message.success('更新成功');
} catch (error) {
$message.error('更新失败');
}
}
function handleModalCancel() {
modalVisible.value = false;
editingId.value = undefined;
}
async function handleModalSubmit(data: AppForm) {
try {
if (editingId.value) {
await updateAppApi(editingId.value, data);
$message.success('更新成功');
} else {
// Create new app would be handled by app store
$message.info('请通过应用商店安装应用');
}
modalVisible.value = false;
await handleRefresh();
} catch (error) {
$message.error(editingId.value ? '更新失败' : '创建失败');
}
}
async function handleRefresh() {
await gridApi.query();
}
function handleExport() {
gridApi.exportData({
filename: '应用列表',
type: 'csv',
});
}
</script>

View File

@@ -0,0 +1,97 @@
<template>
<div>
<VbenForm
:handle-submit="handleSubmit"
:model="model"
:schema="formSchemas"
:show-default-actions="false"
@submit="handleSubmit"
>
<template #form-submit>
<div class="flex items-center justify-end space-x-2">
<VbenButton @click="handleCancel" variant="outline">
{{ $t('common.cancel') }}
</VbenButton>
<VbenButton type="primary" @click="handleSubmit">
{{ $t('common.confirm') }}
</VbenButton>
</div>
</template>
</VbenForm>
</div>
</template>
<script lang="ts" setup>
import type { AppForm } from '../data';
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { useAppFormSchemas } from './formSchemas';
interface Props {
id?: number;
}
interface Emits {
(e: 'submit', data: AppForm): void;
(e: 'cancel'): void;
}
const props = withDefaults(defineProps<Props>(), {
id: undefined,
});
const emit = defineEmits<Emits>();
const [Drawer] = useVbenDrawer();
const model = ref<AppForm>({
name: '',
title: '',
description: '',
author: '',
version: '1.0.0',
icon: '',
cover: '',
preview: '',
path: '',
admin_path: '',
type: 'addon',
category: 'other',
tags: '',
require: '',
install: 0,
status: 1,
config: '',
hooks: '',
});
const formSchemas = useAppFormSchemas();
async function handleSubmit() {
try {
await Drawer?.formApi.validate();
const formValues = Drawer?.formApi.getValues() || model.value;
emit('submit', formValues);
} catch (error) {
console.error('Form validation failed:', error);
}
}
function handleCancel() {
emit('cancel');
}
// Load app data if editing
onMounted(async () => {
if (props.id) {
try {
// Load app data
const appData = await getAppDetailApi(props.id);
model.value = { ...appData };
} catch (error) {
console.error('Failed to load app data:', error);
}
}
});
</script>

View File

@@ -0,0 +1,169 @@
import type { AppForm } from '../data';
import { useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { typeOptions, categoryOptions, statusOptions } from '../data';
export const useAppFormSchemas = () => {
const formSchemas = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '应用标识',
rules: 'required|pattern:^[a-zA-Z][a-zA-Z0-9_]*$',
componentProps: {
placeholder: '请输入应用标识(英文)',
},
},
{
component: 'Input',
fieldName: 'title',
label: '应用名称',
rules: 'required',
componentProps: {
placeholder: '请输入应用名称',
},
},
{
component: 'Textarea',
fieldName: 'description',
label: '应用描述',
componentProps: {
placeholder: '请输入应用描述',
rows: 3,
maxlength: 500,
showCount: true,
},
},
{
component: 'Input',
fieldName: 'author',
label: '作者',
componentProps: {
placeholder: '请输入作者名称',
},
},
{
component: 'Input',
fieldName: 'version',
label: '版本号',
rules: 'required',
componentProps: {
placeholder: '请输入版本号1.0.0',
},
},
{
component: 'Upload',
fieldName: 'icon',
label: '应用图标',
componentProps: {
accept: 'image/*',
maxCount: 1,
showUploadList: true,
listType: 'picture-card',
},
},
{
component: 'Upload',
fieldName: 'cover',
label: '应用封面',
componentProps: {
accept: 'image/*',
maxCount: 1,
showUploadList: true,
listType: 'picture-card',
},
},
{
component: 'Input',
fieldName: 'preview',
label: '预览图',
componentProps: {
placeholder: '请输入预览图URL',
},
},
{
component: 'Input',
fieldName: 'path',
label: '前台路径',
componentProps: {
placeholder: '请输入前台访问路径',
},
},
{
component: 'Input',
fieldName: 'admin_path',
label: '后台路径',
componentProps: {
placeholder: '请输入后台管理路径',
},
},
{
component: 'Select',
fieldName: 'type',
label: '应用类型',
rules: 'required',
componentProps: {
options: typeOptions,
placeholder: '请选择应用类型',
},
},
{
component: 'Select',
fieldName: 'category',
label: '应用分类',
rules: 'required',
componentProps: {
options: categoryOptions,
placeholder: '请选择应用分类',
},
},
{
component: 'Input',
fieldName: 'tags',
label: '应用标签',
componentProps: {
placeholder: '请输入应用标签,多个用逗号分隔',
},
},
{
component: 'Textarea',
fieldName: 'require',
label: '依赖要求',
componentProps: {
placeholder: '请输入依赖要求PHP>=7.2, MySQL>=5.7',
rows: 2,
},
},
{
component: 'Textarea',
fieldName: 'hooks',
label: '钩子配置',
componentProps: {
placeholder: '请输入钩子配置JSON格式',
rows: 3,
},
},
{
component: 'Textarea',
fieldName: 'config',
label: '配置信息',
componentProps: {
placeholder: '请输入配置信息JSON格式',
rows: 3,
},
},
{
component: 'RadioGroup',
fieldName: 'status',
label: '状态',
defaultValue: 1,
componentProps: {
options: statusOptions,
},
},
]);
return formSchemas;
};

View File

@@ -1,135 +1,92 @@
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface MaterialItem { export interface WechatMaterial {
id: number; id: number;
media_id: string; media_id: string;
type: 'image' | 'voice' | 'video' | 'news'; type: 'image' | 'voice' | 'video' | 'news' | 'thumb';
title?: string; title?: string;
introduction?: string; introduction?: string;
url: string; url: string;
thumb_url?: string; thumb_url?: string;
content?: string;
digest?: string;
show_cover_pic: 0 | 1;
author?: string;
content_source_url?: string;
local_url?: string; local_url?: string;
filename: string;
size: number;
width?: number;
height?: number;
duration?: number;
news_item?: NewsItem[];
status: 0 | 1;
create_time: string; create_time: string;
update_time: string; update_time: string;
} }
export interface NewsItem {
title: string;
author: string;
digest: string;
show_cover_pic: 0 | 1;
content: string;
content_source_url: string;
thumb_media_id: string;
thumb_url: string;
url: string;
}
export interface MaterialForm { export interface MaterialForm {
id?: number; id?: number;
type: 'image' | 'voice' | 'video' | 'news'; type: string;
title?: string; title?: string;
introduction?: string; introduction?: string;
file?: File; url?: string;
news_item?: NewsItem[]; thumb_url?: string;
status: 0 | 1; content?: string;
digest?: string;
show_cover_pic: 0 | 1;
author?: string;
content_source_url?: string;
} }
export const materialTypeOptions = [ export const typeOptions = [
{ label: '图片', value: 'image' }, { label: '图片', value: 'image' },
{ label: '语音', value: 'voice' }, { label: '语音', value: 'voice' },
{ label: '视频', value: 'video' }, { label: '视频', value: 'video' },
{ label: '图文', value: 'news' }, { label: '图文', value: 'news' },
{ label: '缩略图', value: 'thumb' },
]; ];
export const materialTypeMap = { export const showCoverOptions = [
image: '图片', { label: '不显示', value: 0 },
voice: '语音', { label: '显示', value: 1 },
video: '视频',
news: '图文',
};
export const statusOptions = [
{ label: '正常', value: 1 },
{ label: '禁用', value: 0 },
]; ];
export const statusMap = { export const gridOptions: VxeGridProps<WechatMaterial> = {
1: '正常', columns: [
0: '禁用', { type: 'checkbox', width: 50 },
}; { field: 'media_id', title: 'MediaID', width: 180 },
{ field: 'type', title: '类型', width: 100, formatter: ({ cellValue }) => {
export const querySchema = [ const option = typeOptions.find(item => item.value === cellValue);
{ return option?.label || cellValue;
fieldName: 'type', }},
label: '素材类型', { field: 'title', title: '标题', minWidth: 150 },
component: 'Select', { field: 'url', title: 'URL', minWidth: 200, showOverflow: true },
componentProps: { { field: 'thumb_url', title: '缩略图', width: 120, formatter: ({ cellValue }) => {
options: materialTypeOptions, return cellValue ? `<img src="${cellValue}" style="width: 60px; height: 60px; object-fit: cover;" />` : '';
placeholder: '请选择素材类型', } },
{ field: 'create_time', title: '创建时间', width: 180 },
{
field: 'action',
fixed: 'right',
title: '操作',
width: 150,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: WechatMaterial) => {
// This will be handled in the component
},
},
},
}, },
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
}, },
{ toolbarConfig: {
fieldName: 'title', custom: true,
label: '标题', export: true,
component: 'Input', // import: true,
print: true,
refresh: true,
zoom: true,
}, },
{ };
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: statusOptions,
placeholder: '请选择状态',
},
},
{
fieldName: 'create_time',
label: '创建时间',
component: 'DatePicker',
componentProps: {
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
},
];
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80 },
{
field: 'thumb_url',
title: '缩略图',
width: 100,
slots: { default: 'thumb' },
align: 'center',
},
{ field: 'title', title: '标题', minWidth: 150 },
{ field: 'type', title: '类型', width: 100, slots: { default: 'type' } },
{ field: 'filename', title: '文件名', minWidth: 200 },
{ field: 'size', title: '大小', width: 120, slots: { default: 'size' } },
{ field: 'width', title: '宽度', width: 100 },
{ field: 'height', title: '高度', width: 100 },
{ field: 'duration', title: '时长', width: 100, slots: { default: 'duration' } },
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
{ field: 'create_time', title: '创建时间', width: 180 },
{ field: 'update_time', title: '更新时间', width: 180 },
{
field: 'action',
title: '操作',
width: 150,
fixed: 'right',
slots: { default: 'action' },
},
];

View File

@@ -1,235 +1,123 @@
<template> <template>
<div> <div class="m-4">
<VbenVxeGrid <VbenVxeGrid
ref="gridRef"
:form-options="formOptions"
:grid-options="gridOptions" :grid-options="gridOptions"
:grid-events="gridEvents" :query-schema="querySchema"
title="微信素材管理"
@toolbar-button-click="handleToolbarClick"
@cell-operation-click="handleCellOperationClick"
> >
<template #toolbar-tools> <template #toolbar-buttons>
<VbenButton type="primary" @click="handleAdd"> <VbenButton type="primary" @click="handleAdd">
<Plus class="mr-2 h-4 w-4" /> <template #icon>
<PlusOutlined />
</template>
上传素材 上传素材
</VbenButton> </VbenButton>
<VbenButton type="success" @click="handleSync"> <VbenButton type="default" @click="handleSync">
<RefreshCw class="mr-2 h-4 w-4" /> <template #icon>
<SyncOutlined />
</template>
同步素材 同步素材
</VbenButton> </VbenButton>
</template> </template>
<template #thumb="{ row }">
<div class="flex justify-center">
<img
v-if="row.thumb_url"
:src="row.thumb_url"
:alt="row.title"
class="w-16 h-16 object-cover rounded"
@error="handleImageError"
/>
<div
v-else
class="w-16 h-16 bg-gray-100 rounded flex items-center justify-center text-gray-400"
>
<FileText class="w-8 h-8" />
</div>
</div>
</template>
<template #type="{ row }">
<VbenTag :type="getTypeColor(row.type)">
{{ materialTypeMap[row.type] }}
</VbenTag>
</template>
<template #size="{ row }">
{{ formatFileSize(row.size) }}
</template>
<template #duration="{ row }">
{{ row.duration ? formatDuration(row.duration) : '-' }}
</template>
<template #status="{ row }">
<VbenTag :type="row.status === 1 ? 'success' : 'error'">
{{ statusMap[row.status] }}
</VbenTag>
</template>
<template #action="{ row }">
<VbenButton
type="text"
size="small"
@click="handleView(row)"
>
查看
</VbenButton>
<VbenButton
type="text"
size="small"
@click="handleEdit(row)"
>
编辑
</VbenButton>
<VbenPopconfirm
title="确定删除该素材吗?"
@confirm="handleDelete(row)"
>
<VbenButton type="text" size="small" danger>
删除
</VbenButton>
</VbenPopconfirm>
</template>
</VbenVxeGrid> </VbenVxeGrid>
<MaterialUploadModal <MaterialForm
v-model="uploadModalVisible" v-model="drawerVisible"
@reload="reloadTable" :material="currentMaterial"
/> @success="handleRefresh"
<MaterialEditModal
v-model="editModalVisible"
:data="currentData"
@reload="reloadTable"
/>
<MaterialViewModal
v-model="viewModalVisible"
:data="currentData"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref } from 'vue';
import { Plus, RefreshCw, FileText } from '@vben/icons'; import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { VbenButton } from '@vben/common-ui';
import { PlusOutlined, SyncOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import MaterialForm from './modules/material-form.vue';
import { gridOptions, querySchema } from './data';
import { getWechatMaterialList, syncWechatMaterial, deleteWechatMaterial } from '#/api/core/wechat';
import type { WechatMaterial } from './data';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; const drawerVisible = ref(false);
import { VbenButton, VbenMessage, VbenPopconfirm, VbenTag } from '@vben/common-ui'; const currentMaterial = ref<WechatMaterial | null>(null);
import { getWechatMaterialList, deleteWechatMaterial, syncWechatMaterial } from '#/api/core/wechat'; const [VbenVxeGrid, { reload }] = useVbenVxeGrid({
import MaterialUploadModal from './modules/upload-modal.vue'; gridOptions,
import MaterialEditModal from './modules/edit-modal.vue'; querySchema,
import MaterialViewModal from './modules/view-modal.vue'; queryList: async (params) => {
const { data } = await getWechatMaterialList(params);
import type { MaterialItem } from './data'; return {
import { columns, querySchema, materialTypeMap, statusMap } from './data'; data: data.list,
total: data.total,
const uploadModalVisible = ref(false); };
const editModalVisible = ref(false);
const viewModalVisible = ref(false);
const currentData = ref<MaterialItem | null>(null);
const gridRef = ref();
const formOptions = computed(() => ({
schema: querySchema,
showCollapseButton: true,
fieldSize: 'medium',
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
}));
const gridOptions = computed(() => ({
columns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
}, },
proxyConfig: { });
ajax: {
query: async ({ page }, formValues) => {
const params = {
page: page.currentPage,
limit: page.pageSize,
...formValues,
};
return await getWechatMaterialList(params);
},
},
},
rowConfig: {
isHover: true,
},
columnConfig: {
minWidth: 100,
},
}));
const gridEvents = { const handleToolbarClick = (code: string) => {
// 表格事件 switch (code) {
case 'add':
handleAdd();
break;
case 'sync':
handleSync();
break;
default:
break;
}
}; };
function getTypeColor(type: string) { const handleCellOperationClick = (code: string, row: WechatMaterial) => {
const colorMap: Record<string, string> = { switch (code) {
image: 'blue', case 'edit':
voice: 'green', currentMaterial.value = row;
video: 'orange', drawerVisible.value = true;
news: 'purple', break;
}; case 'delete':
return colorMap[type] || 'default'; handleDelete(row);
} break;
default:
function formatFileSize(bytes: number): string { break;
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDuration(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}${remainingSeconds}`;
}
function handleImageError(event: Event) {
const target = event.target as HTMLImageElement;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
}
function handleAdd() {
uploadModalVisible.value = true;
}
function handleView(row: MaterialItem) {
currentData.value = row;
viewModalVisible.value = true;
}
function handleEdit(row: MaterialItem) {
currentData.value = row;
editModalVisible.value = true;
}
async function handleDelete(row: MaterialItem) {
try {
await deleteWechatMaterial(row.id);
VbenMessage.success('删除成功');
reloadTable();
} catch (error) {
VbenMessage.error('删除失败');
} }
} };
async function handleSync() { const handleAdd = () => {
currentMaterial.value = null;
drawerVisible.value = true;
};
const handleSync = async () => {
try { try {
message.loading('正在同步微信素材...');
await syncWechatMaterial(); await syncWechatMaterial();
VbenMessage.success('素材同步成功'); message.success('微信素材同步成功');
reloadTable(); reload();
} catch (error) { } catch (error) {
VbenMessage.error('素材同步失败'); message.error('微信素材同步失败');
console.error('同步素材失败:', error);
} }
} };
function reloadTable() { const handleDelete = async (material: WechatMaterial) => {
gridRef.value?.reload(); try {
} await message.confirm('确定要删除该素材吗?', '删除确认');
message.loading('正在删除素材...');
await deleteWechatMaterial(material.id);
message.success('素材删除成功');
reload();
} catch (error) {
if (error !== 'cancel') {
message.error('素材删除失败');
console.error('删除素材失败:', error);
}
}
};
onMounted(() => { const handleRefresh = () => {
// 初始化 reload();
}); };
</script> </script>

View File

@@ -0,0 +1,77 @@
<template>
<div>
<VbenForm
:handle-submit="handleSubmit"
:model="model"
:schema="formSchemas"
:show-default-actions="false"
@submit="handleSubmit"
>
<template #form-submit>
<div class="flex items-center justify-end space-x-2">
<VbenButton @click="handleCancel" variant="outline">
{{ $t('common.cancel') }}
</VbenButton>
<VbenButton type="primary" @click="handleSubmit">
{{ $t('common.confirm') }}
</VbenButton>
</div>
</template>
</VbenForm>
</div>
</template>
<script lang="ts" setup>
import type { MaterialForm } from '../data';
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { useMaterialFormSchemas } from './formSchemas';
interface Props {
id?: number;
materialData?: any;
}
interface Emits {
(e: 'submit', data: MaterialForm): void;
(e: 'cancel'): void;
}
const props = withDefaults(defineProps<Props>(), {
id: undefined,
materialData: undefined,
});
const emit = defineEmits<Emits>();
const [Drawer] = useVbenDrawer();
const model = ref<MaterialForm>({
type: 'image',
show_cover_pic: 0,
});
const formSchemas = useMaterialFormSchemas();
async function handleSubmit() {
try {
await Drawer?.formApi.validate();
const formValues = Drawer?.formApi.getValues() || model.value;
emit('submit', formValues);
} catch (error) {
console.error('Form validation failed:', error);
}
}
function handleCancel() {
emit('cancel');
}
// Load material data if editing
onMounted(async () => {
if (props.id && props.materialData) {
model.value = { ...props.materialData };
}
});
</script>

View File

@@ -0,0 +1,156 @@
import type { MaterialForm } from '../data';
import { useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { typeOptions, showCoverOptions } from '../data';
export const useMaterialFormSchemas = () => {
const formSchemas = computed(() => [
{
component: 'Select',
fieldName: 'type',
label: '素材类型',
rules: 'required',
componentProps: {
options: typeOptions,
placeholder: '请选择素材类型',
},
},
{
component: 'Input',
fieldName: 'title',
label: '标题',
rules: computed(() => {
const form = useVbenForm().getValues();
return ['video', 'news'].includes(form.type) ? 'required' : '';
}),
ifShow: computed(() => {
const form = useVbenForm().getValues();
return ['video', 'news'].includes(form.type);
}),
},
{
component: 'Textarea',
fieldName: 'introduction',
label: '简介',
componentProps: {
placeholder: '请输入简介',
maxlength: 200,
showCount: true,
rows: 3,
},
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'video';
}),
},
{
component: 'Upload',
fieldName: 'url',
label: '素材文件',
rules: 'required',
componentProps: {
accept: computed(() => {
const form = useVbenForm().getValues();
switch (form.type) {
case 'image':
return 'image/*';
case 'voice':
return 'audio/*';
case 'video':
return 'video/*';
case 'thumb':
return 'image/*';
default:
return '*';
}
}),
maxCount: 1,
showUploadList: true,
},
},
{
component: 'Upload',
fieldName: 'thumb_url',
label: '缩略图',
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'video';
}),
componentProps: {
accept: 'image/*',
maxCount: 1,
showUploadList: true,
},
},
{
component: 'Textarea',
fieldName: 'content',
label: '图文内容',
rules: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'news' ? 'required' : '';
}),
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'news';
}),
componentProps: {
placeholder: '请输入图文内容',
rows: 6,
},
},
{
component: 'Textarea',
fieldName: 'digest',
label: '图文摘要',
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'news';
}),
componentProps: {
placeholder: '请输入图文摘要',
maxlength: 120,
showCount: true,
rows: 3,
},
},
{
component: 'RadioGroup',
fieldName: 'show_cover_pic',
label: '封面显示',
defaultValue: 0,
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'news';
}),
componentProps: {
options: showCoverOptions,
},
},
{
component: 'Input',
fieldName: 'author',
label: '作者',
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'news';
}),
},
{
component: 'Input',
fieldName: 'content_source_url',
label: '原文链接',
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'news';
}),
componentProps: {
placeholder: '请输入原文链接',
},
},
]);
return formSchemas;
};

View File

@@ -0,0 +1,427 @@
<template>
<VbenDrawer
v-model:show="isShow"
:title="drawerTitle"
:loading="loading"
width="700px"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<VbenForm
v-model:model="formModel"
v-model:schema="formSchema"
:label-width="100"
@submit="handleConfirm"
>
<template #fileUpload="{ model, field }">
<div class="upload-container">
<a-upload
v-if="showFileUpload"
:file-list="fileList"
:before-upload="beforeUpload"
:accept="acceptFileTypes"
:multiple="false"
@change="handleFileChange"
>
<a-button>
<UploadOutlined />
选择文件
</a-button>
</a-upload>
<div v-if="uploadedFile" class="file-info">
<div class="file-preview">
<img
v-if="isImageType && uploadedFile.url"
:src="uploadedFile.url"
class="preview-image"
alt="预览"
/>
<div v-else class="file-icon">
<FileTextOutlined />
</div>
</div>
<div class="file-details">
<div class="file-name">{{ uploadedFile.name }}</div>
<div class="file-size">{{ formatFileSize(uploadedFile.size) }}</div>
</div>
</div>
</div>
</template>
</VbenForm>
</VbenDrawer>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { useVbenForm, useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { UploadOutlined, FileTextOutlined } from '@ant-design/icons-vue';
import { uploadWechatMaterial, updateWechatMaterial } from '#/api/core/wechat';
import type { WechatMaterial, MaterialForm } from '../data';
interface Props {
modelValue: boolean;
material?: WechatMaterial | null;
}
interface Emits {
(e: 'update:modelValue', value: boolean): void;
(e: 'success'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const loading = ref(false);
const fileList = ref<any[]>([]);
const uploadedFile = ref<any>(null);
const showFileUpload = computed(() => {
return !props.material || formModel.value.type !== props.material.type;
});
const isImageType = computed(() => {
return formModel.value.type === 'image' || formModel.value.type === 'thumb';
});
const acceptFileTypes = computed(() => {
switch (formModel.value.type) {
case 'image':
case 'thumb':
return 'image/*';
case 'voice':
return 'audio/*';
case 'video':
return 'video/*';
default:
return '*';
}
});
const drawerTitle = computed(() => {
return props.material ? '编辑素材' : '上传素材';
});
const [VbenForm, formModel, formSchema] = useVbenForm({
schema: [
{
fieldName: 'type',
label: '素材类型',
component: 'Select',
componentProps: {
placeholder: '请选择素材类型',
options: [
{ label: '图片', value: 'image' },
{ label: '语音', value: 'voice' },
{ label: '视频', value: 'video' },
{ label: '图文', value: 'news' },
{ label: '缩略图', value: 'thumb' },
],
},
rules: 'required',
},
{
fieldName: 'file',
label: '文件上传',
component: 'Slot',
slot: 'fileUpload',
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type !== 'news',
},
},
{
fieldName: 'title',
label: '标题',
component: 'Input',
componentProps: {
placeholder: '请输入标题',
maxlength: 100,
showCount: true,
},
rules: 'required',
},
{
fieldName: 'introduction',
label: '简介',
component: 'InputTextArea',
componentProps: {
placeholder: '请输入简介',
rows: 3,
maxlength: 200,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'video',
},
},
{
fieldName: 'content',
label: '内容',
component: 'InputTextArea',
componentProps: {
placeholder: '请输入图文内容',
rows: 8,
maxlength: 2000,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'news',
},
},
{
fieldName: 'digest',
label: '摘要',
component: 'InputTextArea',
componentProps: {
placeholder: '请输入摘要',
rows: 2,
maxlength: 120,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'news',
},
},
{
fieldName: 'author',
label: '作者',
component: 'Input',
componentProps: {
placeholder: '请输入作者',
maxlength: 50,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'news',
},
},
{
fieldName: 'content_source_url',
label: '原文链接',
component: 'Input',
componentProps: {
placeholder: '请输入原文链接',
maxlength: 200,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'news',
},
},
{
fieldName: 'show_cover_pic',
label: '显示封面',
component: 'RadioGroup',
componentProps: {
options: [
{ label: '不显示', value: 0 },
{ label: '显示', value: 1 },
],
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'news',
},
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
});
const [VbenDrawer, isShow] = useVbenDrawer({
formModel,
formSchema,
});
// 监听props变化
watch(
() => props.modelValue,
(val) => {
isShow.value = val;
if (val) {
if (props.material) {
// 编辑模式
formModel.value = {
id: props.material.id,
type: props.material.type,
title: props.material.title || '',
introduction: props.material.introduction || '',
content: props.material.content || '',
digest: props.material.digest || '',
author: props.material.author || '',
content_source_url: props.material.content_source_url || '',
show_cover_pic: props.material.show_cover_pic || 0,
};
uploadedFile.value = {
url: props.material.url,
name: props.material.title || '已上传文件',
};
} else {
// 新增模式
formModel.value = {
type: 'image',
title: '',
introduction: '',
content: '',
digest: '',
author: '',
content_source_url: '',
show_cover_pic: 0,
};
uploadedFile.value = null;
fileList.value = [];
}
}
},
{ immediate: true },
);
watch(isShow, (val) => {
emit('update:modelValue', val);
});
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const beforeUpload = (file: File) => {
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('文件大小不能超过 2MB!');
return false;
}
return true;
};
const handleFileChange = (info: any) => {
const file = info.file;
if (file.status === 'done' || file.originFileObj) {
const reader = new FileReader();
reader.onload = (e) => {
uploadedFile.value = {
name: file.name,
size: file.size,
url: e.target?.result as string,
file: file.originFileObj || file,
};
};
if (isImageType.value) {
reader.readAsDataURL(file.originFileObj || file);
} else {
uploadedFile.value = {
name: file.name,
size: file.size,
file: file.originFileObj || file,
};
}
}
};
const handleConfirm = async () => {
try {
loading.value = true;
// 构建提交数据
const data: MaterialForm = {
...formModel.value,
};
// 如果有文件需要上传
if (uploadedFile.value?.file && formModel.value.type !== 'news') {
const formData = new FormData();
formData.append('file', uploadedFile.value.file);
formData.append('type', formModel.value.type);
formData.append('title', formModel.value.title);
if (formModel.value.introduction) {
formData.append('introduction', formModel.value.introduction);
}
await uploadWechatMaterial(formData);
message.success('素材上传成功');
} else if (props.material) {
// 编辑模式
await updateWechatMaterial(data);
message.success('素材更新成功');
} else if (formModel.value.type === 'news') {
// 图文消息新增
await uploadWechatMaterial(data);
message.success('图文消息创建成功');
}
emit('success');
isShow.value = false;
} catch (error) {
message.error('操作失败');
console.error('操作失败:', error);
} finally {
loading.value = false;
}
};
const handleCancel = () => {
isShow.value = false;
};
</script>
<style scoped>
.upload-container {
width: 100%;
}
.file-info {
margin-top: 16px;
padding: 16px;
background-color: #f5f5f5;
border-radius: 6px;
display: flex;
align-items: center;
gap: 16px;
}
.file-preview {
flex-shrink: 0;
}
.preview-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.file-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background-color: #e6f7ff;
border-radius: 4px;
font-size: 32px;
color: #1890ff;
}
.file-details {
flex: 1;
}
.file-name {
font-weight: 500;
margin-bottom: 4px;
word-break: break-all;
}
.file-size {
color: #666;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<div class="p-6">
<div class="mb-4">
<h3 class="text-lg font-medium mb-2">素材详情</h3>
<div class="text-sm text-gray-600">
<p><strong>MediaID:</strong> {{ materialData.media_id }}</p>
<p><strong>类型:</strong> {{ getTypeLabel(materialData.type) }}</p>
<p><strong>创建时间:</strong> {{ materialData.create_time }}</p>
</div>
</div>
<div class="mb-4">
<h4 class="font-medium mb-2">基本信息</h4>
<div class="bg-gray-50 p-4 rounded-lg">
<div v-if="materialData.title" class="mb-2">
<strong>标题:</strong> {{ materialData.title }}
</div>
<div v-if="materialData.author" class="mb-2">
<strong>作者:</strong> {{ materialData.author }}
</div>
<div v-if="materialData.digest" class="mb-2">
<strong>摘要:</strong> {{ materialData.digest }}
</div>
<div v-if="materialData.introduction" class="mb-2">
<strong>简介:</strong> {{ materialData.introduction }}
</div>
</div>
</div>
<div v-if="materialData.type === 'image'" class="mb-4">
<h4 class="font-medium mb-2">图片预览</h4>
<div class="text-center">
<img
:src="materialData.url"
alt="图片素材"
class="max-w-md max-h-96 object-contain border rounded-lg mx-auto"
@error="(e: any) => e.target.src = 'https://via.placeholder.com/400x300'"
/>
</div>
</div>
<div v-if="materialData.type === 'video'" class="mb-4">
<h4 class="font-medium mb-2">视频预览</h4>
<div class="text-center">
<video
:src="materialData.url"
controls
class="max-w-md max-h-96 border rounded-lg mx-auto"
>
您的浏览器不支持视频播放
</video>
</div>
<div v-if="materialData.thumb_url" class="mt-2 text-center">
<p class="text-sm text-gray-600 mb-1">缩略图</p>
<img
:src="materialData.thumb_url"
alt="缩略图"
class="w-20 h-20 object-cover border rounded"
@error="(e: any) => e.target.src = 'https://via.placeholder.com/80x80'"
/>
</div>
</div>
<div v-if="materialData.type === 'voice'" class="mb-4">
<h4 class="font-medium mb-2">音频预览</h4>
<div class="text-center">
<audio
:src="materialData.url"
controls
class="max-w-md mx-auto"
>
您的浏览器不支持音频播放
</audio>
</div>
</div>
<div v-if="materialData.type === 'news'" class="mb-4">
<h4 class="font-medium mb-2">图文内容</h4>
<div class="bg-white border rounded-lg p-4">
<div v-if="materialData.show_cover_pic === 1 && materialData.url" class="mb-4">
<img
:src="materialData.url"
alt="封面图"
class="w-full h-48 object-cover rounded"
@error="(e: any) => e.target.src = 'https://via.placeholder.com/400x200'"
/>
</div>
<div
v-if="materialData.content"
class="prose max-w-none"
v-html="materialData.content"
></div>
<div v-if="materialData.content_source_url" class="mt-4">
<a
:href="materialData.content_source_url"
target="_blank"
class="text-blue-600 hover:text-blue-800 underline"
>
阅读原文
</a>
</div>
</div>
</div>
<div class="flex justify-end space-x-2">
<VbenButton @click="handleClose" variant="outline">
{{ $t('common.close') }}
</VbenButton>
</div>
</div>
</template>
<script lang="ts" setup>
import { VbenButton } from '@vben/common-ui';
import { $t } from '@vben/locale';
interface Props {
materialData: any;
}
interface Emits {
(e: 'cancel'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
function getTypeLabel(type: string): string {
const typeMap: Record<string, string> = {
'image': '图片',
'voice': '语音',
'video': '视频',
'news': '图文',
'thumb': '缩略图',
};
return typeMap[type] || type;
}
function handleClose() {
emit('cancel');
}
</script>

View File

@@ -1,4 +1,4 @@
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface MenuItem { export interface MenuItem {
id: number; id: number;
@@ -6,35 +6,35 @@ export interface MenuItem {
type: 'click' | 'view' | 'miniprogram' | 'scancode_push' | 'scancode_waitmsg' | 'pic_sysphoto' | 'pic_photo_or_album' | 'pic_weixin' | 'location_select'; type: 'click' | 'view' | 'miniprogram' | 'scancode_push' | 'scancode_waitmsg' | 'pic_sysphoto' | 'pic_photo_or_album' | 'pic_weixin' | 'location_select';
key?: string; key?: string;
url?: string; url?: string;
media_id?: string;
appid?: string; appid?: string;
pagepath?: string; pagepath?: string;
media_id?: string;
parent_id: number; parent_id: number;
sort: number; sort: number;
status: 0 | 1; status: 0 | 1;
create_time: string; create_time: string;
update_time: string; children?: MenuItem[];
} }
export interface MenuForm { export interface MenuForm {
id?: number; id?: number;
name: string; name: string;
type: 'click' | 'view' | 'miniprogram' | 'scancode_push' | 'scancode_waitmsg' | 'pic_sysphoto' | 'pic_photo_or_album' | 'pic_weixin' | 'location_select'; type: string;
key?: string; key?: string;
url?: string; url?: string;
media_id?: string;
appid?: string; appid?: string;
pagepath?: string; pagepath?: string;
media_id?: string;
parent_id: number; parent_id: number;
sort: number; sort: number;
status: 0 | 1; status: 0 | 1;
} }
export const menuTypeOptions = [ export const typeOptions = [
{ label: '点击推事件', value: 'click' }, { label: '点击推事件', value: 'click' },
{ label: '跳转URL', value: 'view' }, { label: '跳转URL', value: 'view' },
{ label: '扫码推事件', value: 'scancode_push' }, { label: '扫码推事件', value: 'scancode_push' },
{ label: '扫码推事件且弹出提示', value: 'scancode_waitmsg' }, { label: '扫码推事件且弹出消息接收中', value: 'scancode_waitmsg' },
{ label: '弹出系统拍照发图', value: 'pic_sysphoto' }, { label: '弹出系统拍照发图', value: 'pic_sysphoto' },
{ label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' }, { label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' },
{ label: '弹出微信相册发图器', value: 'pic_weixin' }, { label: '弹出微信相册发图器', value: 'pic_weixin' },
@@ -42,70 +42,60 @@ export const menuTypeOptions = [
{ label: '跳转小程序', value: 'miniprogram' }, { label: '跳转小程序', value: 'miniprogram' },
]; ];
export const menuTypeMap = {
click: '点击推事件',
view: '跳转URL',
scancode_push: '扫码推事件',
scancode_waitmsg: '扫码推事件且弹出提示',
pic_sysphoto: '弹出系统拍照发图',
pic_photo_or_album: '弹出拍照或者相册发图',
pic_weixin: '弹出微信相册发图器',
location_select: '弹出地理位置选择器',
miniprogram: '跳转小程序',
};
export const statusOptions = [ export const statusOptions = [
{ label: '启用', value: 1 }, { label: '启用', value: 1 },
{ label: '禁用', value: 0 }, { label: '禁用', value: 0 },
]; ];
export const statusMap = { export const gridOptions: VxeGridProps<MenuItem> = {
1: '启用', columns: [
0: '禁用', { type: 'checkbox', width: 50 },
}; { field: 'name', title: '菜单名称', minWidth: 150, treeNode: true },
{ field: 'type', title: '菜单类型', width: 120, formatter: ({ cellValue }) => {
export const querySchema = [ const option = typeOptions.find(item => item.value === cellValue);
{ return option?.label || cellValue;
fieldName: 'name', }},
label: '菜单名称', { field: 'key', title: '菜单KEY', width: 150 },
component: 'Input', { field: 'url', title: '跳转URL', minWidth: 200 },
{ field: 'sort', title: '排序', width: 80 },
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
return cellValue === 1 ? '启用' : '禁用';
}},
{ field: 'create_time', title: '创建时间', width: 180 },
{
field: 'action',
fixed: 'right',
title: '操作',
width: 150,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: MenuItem) => {
// This will be handled in the component
},
},
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
}, },
{ proxyConfig: {
fieldName: 'type', ajax: {
label: '菜单类型', query: async ({ page }, formValues = {}) => {
component: 'Select', // This will be implemented in the component
componentProps: { return { rows: [], total: 0 };
options: menuTypeOptions, },
placeholder: '请选择菜单类型',
}, },
}, },
{ toolbarConfig: {
fieldName: 'status', custom: true,
label: '状态', export: true,
component: 'Select', // import: true,
componentProps: { print: true,
options: statusOptions, refresh: true,
placeholder: '请选择状态', zoom: true,
},
}, },
]; };
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80 },
{ field: 'name', title: '菜单名称', minWidth: 150 },
{ field: 'type', title: '菜单类型', width: 150, slots: { default: 'menuType' } },
{ field: 'key', title: '菜单KEY', width: 150 },
{ field: 'url', title: '跳转URL', minWidth: 200, showOverflow: true },
{ field: 'sort', title: '排序', width: 80 },
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
{ field: 'create_time', title: '创建时间', width: 180 },
{ field: 'update_time', title: '更新时间', width: 180 },
{
field: 'action',
title: '操作',
width: 150,
fixed: 'right',
slots: { default: 'action' },
},
];

View File

@@ -1,176 +1,156 @@
<template> <template>
<div> <div class="h-full">
<VbenVxeGrid <VbenVxeGrid
ref="gridRef"
:form-options="formOptions"
:grid-options="gridOptions" :grid-options="gridOptions"
:grid-events="gridEvents" :query-schema="querySchema"
title="自定义菜单管理"
@toolbar-button-click="handleToolbarClick"
> >
<template #toolbar-tools> <template #toolbar-tools>
<VbenButton type="primary" @click="handleAdd"> <Button type="primary" @click="handleSync">
<Plus class="mr-2 h-4 w-4" /> <Icon icon="ant-design:sync-outlined" />
新增菜单 同步菜单
</VbenButton> </Button>
<VbenButton type="success" @click="handlePublish"> <Button type="primary" @click="handlePublish">
<Upload class="mr-2 h-4 w-4" /> <Icon icon="ant-design:cloud-upload-outlined" />
发布菜单 发布菜单
</VbenButton> </Button>
</template>
<template #menuType="{ row }">
<VbenTag :type="getMenuTypeColor(row.type)">
{{ menuTypeMap[row.type] }}
</VbenTag>
</template>
<template #status="{ row }">
<VbenTag :type="row.status === 1 ? 'success' : 'error'">
{{ statusMap[row.status] }}
</VbenTag>
</template>
<template #action="{ row }">
<VbenButton
type="text"
size="small"
@click="handleEdit(row)"
>
编辑
</VbenButton>
<VbenPopconfirm
title="确定删除该菜单吗?"
@confirm="handleDelete(row)"
>
<VbenButton type="text" size="small" danger>
删除
</VbenButton>
</VbenPopconfirm>
</template> </template>
</VbenVxeGrid> </VbenVxeGrid>
<MenuEditModal <VbenDrawer
v-model="modalVisible" v-model:show="drawerShow"
:data="currentData" :title="drawerTitle"
:parent-menus="parentMenus" :width="800"
@reload="reloadTable" >
/> <MenuForm
:id="currentId"
@success="handleSuccess"
@cancel="handleCancel"
/>
</VbenDrawer>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref } from 'vue';
import { Plus, Upload } from '@vben/icons'; import { Button } from 'ant-design-vue';
import { Icon } from '@iconify/vue';
import { useVbenVxeGrid, VbenDrawer } from '#/adapter';
import { useVbenForm } from '#/adapter/form';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { gridOptions } from './data';
import { VbenButton, VbenMessage, VbenPopconfirm, VbenTag } from '@vben/common-ui'; import MenuForm from './modules/menu-form.vue';
import { deleteWechatMenu, syncWechatMenu, publishWechatMenu } from '#/api';
import { getWechatMenuList, deleteWechatMenu, publishWechatMenu } from '#/api/core/wechat'; const drawerShow = ref(false);
import MenuEditModal from './modules/menu-edit.vue'; const drawerTitle = ref('');
const currentId = ref<number | null>(null);
import type { MenuItem } from './data'; const querySchema = [
import { columns, querySchema, menuTypeMap, statusMap } from './data'; {
fieldName: 'name',
const modalVisible = ref(false); label: '菜单名称',
const currentData = ref<MenuItem | null>(null); component: 'Input',
const menuList = ref<MenuItem[]>([]); componentProps: {
placeholder: '请输入菜单名称',
const gridRef = ref();
const formOptions = computed(() => ({
schema: querySchema,
showCollapseButton: false,
fieldSize: 'medium',
wrapperClass: 'grid-cols-1 md:grid-cols-3 lg:grid-cols-4',
}));
const gridOptions = computed(() => ({
columns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const params = {
page: page.currentPage,
limit: page.pageSize,
...formValues,
};
const response = await getWechatMenuList(params);
menuList.value = response.data;
return response;
},
}, },
}, },
rowConfig: { {
isHover: true, fieldName: 'type',
label: '菜单类型',
component: 'Select',
componentProps: {
placeholder: '请选择菜单类型',
options: [
{ label: '点击推事件', value: 'click' },
{ label: '跳转URL', value: 'view' },
{ label: '扫码推事件', value: 'scancode_push' },
{ label: '扫码推事件且弹出消息接收中', value: 'scancode_waitmsg' },
{ label: '弹出系统拍照发图', value: 'pic_sysphoto' },
{ label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' },
{ label: '弹出微信相册发图器', value: 'pic_weixin' },
{ label: '弹出地理位置选择器', value: 'location_select' },
{ label: '跳转小程序', value: 'miniprogram' },
],
},
}, },
columnConfig: { {
minWidth: 100, fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
placeholder: '请选择状态',
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
}, },
})); ];
const gridEvents = { function handleToolbarClick(code: string, row: any) {
// 表格事件 switch (code) {
}; case 'add':
handleAdd();
const parentMenus = computed(() => { break;
return menuList.value.filter(item => item.parent_id === 0); case 'edit':
}); handleEdit(row);
break;
function getMenuTypeColor(type: string) { case 'delete':
const colorMap: Record<string, string> = { handleDelete(row);
click: 'blue', break;
view: 'green', default:
miniprogram: 'orange', break;
scancode_push: 'purple', }
scancode_waitmsg: 'purple',
pic_sysphoto: 'pink',
pic_photo_or_album: 'pink',
pic_weixin: 'pink',
location_select: 'cyan',
};
return colorMap[type] || 'default';
} }
function handleAdd() { function handleAdd() {
currentData.value = null; drawerTitle.value = '新增菜单';
modalVisible.value = true; currentId.value = null;
drawerShow.value = true;
} }
function handleEdit(row: MenuItem) { function handleEdit(row: any) {
currentData.value = row; drawerTitle.value = '编辑菜单';
modalVisible.value = true; currentId.value = row.id;
drawerShow.value = true;
} }
async function handleDelete(row: MenuItem) { async function handleDelete(row: any) {
try { try {
await deleteWechatMenu(row.id); await deleteWechatMenu(row.id);
VbenMessage.success('删除成功'); // Refresh grid
reloadTable();
} catch (error) { } catch (error) {
VbenMessage.error('删除失败'); console.error('删除菜单失败:', error);
}
}
async function handleSync() {
try {
await syncWechatMenu();
// Refresh grid
} catch (error) {
console.error('同步菜单失败:', error);
} }
} }
async function handlePublish() { async function handlePublish() {
try { try {
await publishWechatMenu(); await publishWechatMenu();
VbenMessage.success('菜单发布成功'); // Refresh grid
} catch (error) { } catch (error) {
VbenMessage.error('菜单发布失败'); console.error('发布菜单失败:', error);
} }
} }
function reloadTable() { function handleSuccess() {
gridRef.value?.reload(); drawerShow.value = false;
// Refresh grid
} }
onMounted(() => { function handleCancel() {
// 初始化 drawerShow.value = false;
}); }
</script> </script>

View File

@@ -0,0 +1,114 @@
<template>
<div>
<VbenForm
:handle-submit="handleSubmit"
:model="model"
:schema="formSchemas"
:show-default-actions="false"
@submit="handleSubmit"
>
<template #form-submit>
<div class="flex items-center justify-end space-x-2">
<VbenButton @click="handleCancel" variant="outline">
{{ $t('common.cancel') }}
</VbenButton>
<VbenButton type="primary" @click="handleSubmit">
{{ $t('common.confirm') }}
</VbenButton>
</div>
</template>
</VbenForm>
</div>
</template>
<script lang="ts" setup>
import type { MenuForm } from '../data';
import { computed } from 'vue';
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { useMenuFormSchemas } from './formSchemas';
interface Props {
id?: number;
menuTree: any[];
}
interface Emits {
(e: 'submit', data: MenuForm): void;
(e: 'cancel'): void;
}
const props = withDefaults(defineProps<Props>(), {
id: undefined,
});
const emit = defineEmits<Emits>();
const [Drawer] = useVbenDrawer();
const model = ref<MenuForm>({
name: '',
type: 'click',
parent_id: 0,
sort: 0,
status: 1,
});
const formSchemas = useMenuFormSchemas();
const treeData = computed(() => {
const tree = props.menuTree.map(item => ({
title: item.name,
value: item.id,
key: item.id,
children: item.children?.map(child => ({
title: child.name,
value: child.id,
key: child.id,
})) || [],
}));
return [
{ title: '顶级菜单', value: 0, key: 0 },
...tree,
];
});
// Update form schemas with dynamic tree data
watchEffect(() => {
const schemas = formSchemas.value;
const parentSchema = schemas.find(schema => schema.fieldName === 'parent_id');
if (parentSchema && parentSchema.componentProps) {
parentSchema.componentProps.treeData = treeData.value;
}
});
async function handleSubmit() {
try {
await Drawer?.formApi.validate();
const formValues = Drawer?.formApi.getValues() || model.value;
emit('submit', formValues);
} catch (error) {
console.error('Form validation failed:', error);
}
}
function handleCancel() {
emit('cancel');
}
// Load menu data if editing
onMounted(async () => {
if (props.id) {
try {
// Load menu data
const menuData = await getWechatMenuDetailApi(props.id);
model.value = { ...menuData };
} catch (error) {
console.error('Failed to load menu data:', error);
}
}
});
</script>

View File

@@ -0,0 +1,111 @@
import type { MenuForm, MenuItem } from './data';
import { useVbenForm } from '@vben/common-ui';
import { getI18nOptions } from '@vben/locale';
import { $t } from '@vben/locale';
import { typeOptions, statusOptions } from './data';
export const useMenuFormSchemas = () => {
const formSchemas = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '菜单名称',
rules: 'required',
},
{
component: 'Select',
fieldName: 'type',
label: '菜单类型',
rules: 'required',
componentProps: {
options: typeOptions,
placeholder: '请选择菜单类型',
},
},
{
component: 'Input',
fieldName: 'key',
label: '菜单KEY',
rules: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'click' ? 'required' : '';
}),
ifShow: computed(() => {
const form = useVbenForm().getValues();
return ['click', 'scancode_push', 'scancode_waitmsg', 'pic_sysphoto', 'pic_photo_or_album', 'pic_weixin', 'location_select'].includes(form.type);
}),
},
{
component: 'Input',
fieldName: 'url',
label: '跳转URL',
rules: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'view' ? 'required|url' : '';
}),
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'view';
}),
},
{
component: 'Input',
fieldName: 'appid',
label: '小程序AppID',
rules: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'miniprogram' ? 'required' : '';
}),
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'miniprogram';
}),
},
{
component: 'Input',
fieldName: 'pagepath',
label: '小程序页面路径',
rules: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'miniprogram' ? 'required' : '';
}),
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'miniprogram';
}),
},
{
component: 'TreeSelect',
fieldName: 'parent_id',
label: '上级菜单',
componentProps: {
placeholder: '请选择上级菜单',
treeDefaultExpandAll: false,
allowClear: true,
},
},
{
component: 'InputNumber',
fieldName: 'sort',
label: '排序',
defaultValue: 0,
componentProps: {
min: 0,
max: 999,
},
},
{
component: 'RadioGroup',
fieldName: 'status',
label: '状态',
defaultValue: 1,
componentProps: {
options: statusOptions,
},
},
]);
return formSchemas;
};

View File

@@ -0,0 +1,225 @@
<template>
<VbenForm
:schema="formSchema"
:handle-submit="handleSubmit"
:submit-button-options="{ text: '保存' }"
:reset-button-options="{ show: false }"
wrapper-class="!grid-cols-1 md:!grid-cols-2"
/>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { $t } from '#/locales';
import { getWechatMenuInfo, createWechatMenu, updateWechatMenu } from '#/api';
interface Props {
id?: number | null;
}
const props = withDefaults(defineProps<Props>(), {
id: null,
});
const emit = defineEmits<{
success: [];
cancel: [];
}>();
const loading = ref(false);
const menuInfo = ref<any>(null);
const typeOptions = [
{ label: '点击推事件', value: 'click' },
{ label: '跳转URL', value: 'view' },
{ label: '扫码推事件', value: 'scancode_push' },
{ label: '扫码推事件且弹出消息接收中', value: 'scancode_waitmsg' },
{ label: '弹出系统拍照发图', value: 'pic_sysphoto' },
{ label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' },
{ label: '弹出微信相册发图器', value: 'pic_weixin' },
{ label: '弹出地理位置选择器', value: 'location_select' },
{ label: '跳转小程序', value: 'miniprogram' },
];
const statusOptions = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
];
const formSchema = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '菜单名称',
rules: 'required|max:16',
componentProps: {
placeholder: '请输入菜单名称不超过16个字节',
maxLength: 16,
showCount: true,
},
},
{
component: 'Select',
fieldName: 'type',
label: '菜单类型',
rules: 'required',
componentProps: {
placeholder: '请选择菜单类型',
options: typeOptions,
},
},
{
component: 'Input',
fieldName: 'key',
label: '菜单KEY',
rules: 'max:128',
componentProps: {
placeholder: '请输入菜单KEY不超过128字节',
maxLength: 128,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'click',
},
},
{
component: 'Input',
fieldName: 'url',
label: '跳转URL',
rules: 'required|url|max:1024',
componentProps: {
placeholder: '请输入跳转URL不超过1024字节',
maxLength: 1024,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'view',
},
},
{
component: 'Input',
fieldName: 'media_id',
label: '媒体ID',
rules: 'max:64',
componentProps: {
placeholder: '请输入媒体ID不超过64字节',
maxLength: 64,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => ['pic_sysphoto', 'pic_photo_or_album', 'pic_weixin'].includes(type),
},
},
{
component: 'Input',
fieldName: 'appid',
label: '小程序APPID',
rules: 'max:32',
componentProps: {
placeholder: '请输入小程序APPID不超过32字节',
maxLength: 32,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'miniprogram',
},
},
{
component: 'Input',
fieldName: 'pagepath',
label: '小程序页面路径',
rules: 'max:128',
componentProps: {
placeholder: '请输入小程序页面路径不超过128字节',
maxLength: 128,
showCount: true,
},
dependencies: {
triggerFields: ['type'],
if: ({ type }) => type === 'miniprogram',
},
},
{
component: 'TreeSelect',
fieldName: 'parent_id',
label: '上级菜单',
componentProps: {
placeholder: '请选择上级菜单,不选则为一级菜单',
treeData: [], // This will be loaded from API
fieldNames: {
label: 'name',
value: 'id',
},
allowClear: true,
},
},
{
component: 'InputNumber',
fieldName: 'sort',
label: '排序',
rules: 'required|integer|min:0',
componentProps: {
placeholder: '请输入排序',
min: 0,
max: 999,
},
},
{
component: 'RadioGroup',
fieldName: 'status',
label: '状态',
rules: 'required',
defaultValue: 1,
componentProps: {
options: statusOptions,
},
},
]);
async function loadMenuInfo() {
if (!props.id) return;
try {
loading.value = true;
const res = await getWechatMenuInfo(props.id);
menuInfo.value = res.data;
} catch (error) {
message.error('获取菜单信息失败');
} finally {
loading.value = false;
}
}
async function handleSubmit(values: any) {
try {
loading.value = true;
const data = {
...values,
id: props.id,
};
if (props.id) {
await updateWechatMenu(data);
message.success('更新菜单成功');
} else {
await createWechatMenu(data);
message.success('创建菜单成功');
}
emit('success');
} catch (error) {
message.error(props.id ? '更新菜单失败' : '创建菜单失败');
} finally {
loading.value = false;
}
}
watch(() => props.id, loadMenuInfo, { immediate: true });
</script>

View File

@@ -1,33 +1,32 @@
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface UserItem { export interface WechatUser {
id: number; id: number;
openid: string; openid: string;
nickname: string; nickname: string;
headimgurl: string;
sex: 0 | 1 | 2; // 0:未知, 1:男, 2:女 sex: 0 | 1 | 2; // 0:未知, 1:男, 2:女
language: string;
city: string; city: string;
province: string; province: string;
country: string; country: string;
headimgurl: string;
subscribe: 0 | 1; // 0:未关注, 1:已关注 subscribe: 0 | 1; // 0:未关注, 1:已关注
subscribe_time: string; subscribe_time: string;
unsubscribe_time?: string; unsubscribe_time?: string;
unionid?: string; unionid?: string;
remark: string;
groupid: number; groupid: number;
tagid_list: string; tagid_list: string;
subscribe_scene: string; subscribe_scene: string;
qr_scene?: string; qr_scene: string;
qr_scene_str?: string; qr_scene_str: string;
remark: string;
language: string;
create_time: string; create_time: string;
update_time: string; update_time: string;
} }
export interface UserForm { export interface WechatUserForm {
id?: number; id?: number;
openid: string; remark: string;
remark?: string;
groupid?: number; groupid?: number;
} }
@@ -37,109 +36,60 @@ export const sexOptions = [
{ label: '女', value: 2 }, { label: '女', value: 2 },
]; ];
export const sexMap = {
0: '未知',
1: '男',
2: '女',
};
export const subscribeOptions = [ export const subscribeOptions = [
{ label: '全部', value: '' },
{ label: '已关注', value: 1 }, { label: '已关注', value: 1 },
{ label: '未关注', value: 0 }, { label: '未关注', value: 0 },
]; ];
export const subscribeMap = { export const gridOptions: VxeGridProps<WechatUser> = {
1: '已关注', columns: [
0: '未关注', { type: 'checkbox', width: 50 },
}; { field: 'headimgurl', title: '头像', width: 80, formatter: ({ cellValue }) => {
return cellValue ? `<img src="${cellValue}" style="width: 40px; height: 40px; border-radius: 50%;" />` : '';
export const querySchema = [ } },
{ { field: 'nickname', title: '昵称', minWidth: 120 },
fieldName: 'nickname', { field: 'sex', title: '性别', width: 80, formatter: ({ cellValue }) => {
label: '昵称', const option = sexOptions.find(item => item.value === cellValue);
component: 'Input', return option?.label || '未知';
}, }},
{ { field: 'city', title: '城市', width: 100 },
fieldName: 'openid', { field: 'province', title: '省份', width: 100 },
label: 'OpenID', { field: 'country', title: '国家', width: 100 },
component: 'Input', { field: 'subscribe', title: '关注状态', width: 100, formatter: ({ cellValue }) => {
}, return cellValue === 1 ? '已关注' : '未关注';
{ }},
fieldName: 'sex', { field: 'subscribe_time', title: '关注时间', width: 180 },
label: '性别', { field: 'remark', title: '备注', minWidth: 150 },
component: 'Select', { field: 'groupid', title: '分组ID', width: 100 },
componentProps: { {
options: sexOptions, field: 'action',
placeholder: '请选择性别', fixed: 'right',
title: '操作',
width: 150,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: WechatUser) => {
// This will be handled in the component
},
},
},
}, },
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100, 200],
}, },
{ toolbarConfig: {
fieldName: 'subscribe', custom: true,
label: '关注状态', export: true,
component: 'Select', // import: true,
componentProps: { print: true,
options: subscribeOptions, refresh: true,
placeholder: '请选择关注状态', zoom: true,
},
}, },
{ };
fieldName: 'city',
label: '城市',
component: 'Input',
},
{
fieldName: 'province',
label: '省份',
component: 'Input',
},
{
fieldName: 'country',
label: '国家',
component: 'Input',
},
{
fieldName: 'subscribe_time',
label: '关注时间',
component: 'DatePicker',
componentProps: {
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
},
];
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80 },
{
field: 'headimgurl',
title: '头像',
width: 80,
slots: { default: 'avatar' },
align: 'center',
},
{ field: 'nickname', title: '昵称', minWidth: 150 },
{ field: 'sex', title: '性别', width: 80, slots: { default: 'sex' } },
{ field: 'city', title: '城市', width: 120 },
{ field: 'province', title: '省份', width: 120 },
{ field: 'country', title: '国家', width: 120 },
{ field: 'subscribe', title: '关注状态', width: 100, slots: { default: 'subscribe' } },
{ field: 'subscribe_time', title: '关注时间', width: 180 },
{ field: 'unsubscribe_time', title: '取消关注时间', width: 180 },
{ field: 'remark', title: '备注', minWidth: 150, showOverflow: true },
{ field: 'groupid', title: '分组ID', width: 100 },
{ field: 'tagid_list', title: '标签ID', minWidth: 150, showOverflow: true },
{ field: 'language', title: '语言', width: 100 },
{ field: 'subscribe_scene', title: '关注场景', minWidth: 150, showOverflow: true },
{
field: 'action',
title: '操作',
width: 150,
fixed: 'right',
slots: { default: 'action' },
},
];

View File

@@ -1,180 +1,119 @@
<template> <template>
<div> <div class="m-4">
<VbenVxeGrid <VbenVxeGrid
ref="gridRef"
:form-options="formOptions"
:grid-options="gridOptions" :grid-options="gridOptions"
:grid-events="gridEvents" :query-schema="querySchema"
title="微信用户管理"
@toolbar-button-click="handleToolbarClick"
@cell-operation-click="handleCellOperationClick"
> >
<template #toolbar-tools> <template #toolbar-buttons>
<VbenButton type="primary" @click="handleSync"> <VbenButton type="primary" @click="handleSync">
<RefreshCw class="mr-2 h-4 w-4" /> <template #icon>
<SyncOutlined />
</template>
同步用户 同步用户
</VbenButton> </VbenButton>
</template> <VbenButton type="default" @click="handleExport">
<template #icon>
<template #avatar="{ row }"> <ExportOutlined />
<VbenAvatar :src="row.headimgurl" :alt="row.nickname" size="small" /> </template>
</template> 导出用户
<template #sex="{ row }">
<VbenTag :type="getSexColor(row.sex)">
{{ sexMap[row.sex] }}
</VbenTag>
</template>
<template #subscribe="{ row }">
<VbenTag :type="row.subscribe === 1 ? 'success' : 'error'">
{{ subscribeMap[row.subscribe] }}
</VbenTag>
</template>
<template #action="{ row }">
<VbenButton
type="text"
size="small"
@click="handleEdit(row)"
>
编辑
</VbenButton>
<VbenButton
type="text"
size="small"
@click="handleSendMessage(row)"
>
发消息
</VbenButton>
<VbenButton
type="text"
size="small"
@click="handleMoveGroup(row)"
>
移动分组
</VbenButton> </VbenButton>
</template> </template>
</VbenVxeGrid> </VbenVxeGrid>
<UserEditModal <UserForm
v-model="modalVisible" v-model="drawerVisible"
:data="currentData" :user="currentUser"
@reload="reloadTable" @success="handleRefresh"
/>
<SendMessageModal
v-model="messageModalVisible"
:user="currentData"
@reload="reloadTable"
/>
<MoveGroupModal
v-model="groupModalVisible"
:user="currentData"
@reload="reloadTable"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref } from 'vue';
import { RefreshCw } from '@vben/icons'; import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { VbenButton } from '@vben/common-ui';
import { SyncOutlined, ExportOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import UserForm from './modules/user-form.vue';
import { gridOptions, querySchema } from './data';
import { getWechatUserList, syncWechatUser, exportWechatUser } from '#/api/core/wechat';
import type { WechatUser } from './data';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; const drawerVisible = ref(false);
import { VbenAvatar, VbenButton, VbenMessage, VbenTag } from '@vben/common-ui'; const currentUser = ref<WechatUser | null>(null);
import { getWechatUserList, syncWechatUser } from '#/api/core/wechat'; const [VbenVxeGrid, { reload }] = useVbenVxeGrid({
import UserEditModal from './modules/user-edit.vue'; gridOptions,
import SendMessageModal from './modules/send-message.vue'; querySchema,
import MoveGroupModal from './modules/move-group.vue'; queryList: async (params) => {
const { data } = await getWechatUserList(params);
import type { UserItem } from './data'; return {
import { columns, querySchema, sexMap, subscribeMap } from './data'; data: data.list,
total: data.total,
const modalVisible = ref(false); };
const messageModalVisible = ref(false);
const groupModalVisible = ref(false);
const currentData = ref<UserItem | null>(null);
const gridRef = ref();
const formOptions = computed(() => ({
schema: querySchema,
showCollapseButton: true,
fieldSize: 'medium',
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
}));
const gridOptions = computed(() => ({
columns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
}, },
proxyConfig: { });
ajax: {
query: async ({ page }, formValues) => {
const params = {
page: page.currentPage,
limit: page.pageSize,
...formValues,
};
return await getWechatUserList(params);
},
},
},
rowConfig: {
isHover: true,
},
columnConfig: {
minWidth: 100,
},
}));
const gridEvents = { const handleToolbarClick = (code: string) => {
// 表格事件 switch (code) {
case 'sync':
handleSync();
break;
case 'export':
handleExport();
break;
default:
break;
}
}; };
function getSexColor(sex: number) { const handleCellOperationClick = (code: string, row: WechatUser) => {
const colorMap: Record<number, string> = { switch (code) {
0: 'default', case 'edit':
1: 'blue', currentUser.value = row;
2: 'pink', drawerVisible.value = true;
}; break;
return colorMap[sex] || 'default'; default:
} break;
function handleEdit(row: UserItem) {
currentData.value = row;
modalVisible.value = true;
}
function handleSendMessage(row: UserItem) {
currentData.value = row;
messageModalVisible.value = true;
}
function handleMoveGroup(row: UserItem) {
currentData.value = row;
groupModalVisible.value = true;
}
async function handleSync() {
try {
await syncWechatUser();
VbenMessage.success('用户同步成功');
reloadTable();
} catch (error) {
VbenMessage.error('用户同步失败');
} }
} };
function reloadTable() { const handleSync = async () => {
gridRef.value?.reload(); try {
} message.loading('正在同步微信用户...');
await syncWechatUser();
message.success('微信用户同步成功');
reload();
} catch (error) {
message.error('微信用户同步失败');
console.error('同步用户失败:', error);
}
};
onMounted(() => { const handleExport = async () => {
// 初始化 try {
}); message.loading('正在导出用户数据...');
const { data } = await exportWechatUser();
// 创建下载链接
const link = document.createElement('a');
link.href = data.url;
link.download = '微信用户数据.xlsx';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success('用户数据导出成功');
} catch (error) {
message.error('用户数据导出失败');
console.error('导出用户失败:', error);
}
};
const handleRefresh = () => {
reload();
};
</script> </script>

View File

@@ -0,0 +1,76 @@
<template>
<div>
<VbenForm
:handle-submit="handleSubmit"
:model="model"
:schema="formSchemas"
:show-default-actions="false"
@submit="handleSubmit"
>
<template #form-submit>
<div class="flex items-center justify-end space-x-2">
<VbenButton @click="handleCancel" variant="outline">
{{ $t('common.cancel') }}
</VbenButton>
<VbenButton type="primary" @click="handleSubmit">
{{ $t('common.confirm') }}
</VbenButton>
</div>
</template>
</VbenForm>
</div>
</template>
<script lang="ts" setup>
import type { WechatUserForm } from '../data';
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { useUserFormSchemas } from './formSchemas';
interface Props {
id: number;
userData: any;
}
interface Emits {
(e: 'submit', data: WechatUserForm): void;
(e: 'cancel'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const [Drawer] = useVbenDrawer();
const model = ref<WechatUserForm>({
remark: '',
groupid: 0,
});
const formSchemas = useUserFormSchemas();
async function handleSubmit() {
try {
await Drawer?.formApi.validate();
const formValues = Drawer?.formApi.getValues() || model.value;
emit('submit', formValues);
} catch (error) {
console.error('Form validation failed:', error);
}
}
function handleCancel() {
emit('cancel');
}
// Load user data
onMounted(async () => {
if (props.userData) {
model.value = {
remark: props.userData.remark || '',
groupid: props.userData.groupid || 0,
};
}
});
</script>

View File

@@ -0,0 +1,30 @@
import type { WechatUserForm } from '../data';
import { useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locale';
export const useUserFormSchemas = () => {
const formSchemas = computed(() => [
{
component: 'Input',
fieldName: 'remark',
label: '备注',
componentProps: {
placeholder: '请输入备注',
maxlength: 100,
showCount: true,
},
},
{
component: 'InputNumber',
fieldName: 'groupid',
label: '分组ID',
componentProps: {
placeholder: '请输入分组ID',
min: 0,
},
},
]);
return formSchemas;
};

View File

@@ -0,0 +1,144 @@
<template>
<VbenDrawer
v-model:show="isShow"
:title="$t('channel.wechat.user.edit')"
:loading="loading"
width="600px"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<VbenForm
v-model:model="formModel"
v-model:schema="formSchema"
:label-width="100"
@submit="handleConfirm"
>
<template #avatar="{ model, field }">
<div class="flex items-center space-x-4">
<img
v-if="model.headimgurl"
:src="model.headimgurl"
class="w-16 h-16 rounded-full border-2 border-gray-200"
alt="用户头像"
/>
<div v-if="model.nickname" class="text-sm text-gray-600">
<div class="font-medium">{{ model.nickname }}</div>
<div>OpenID: {{ model.openid }}</div>
</div>
</div>
</template>
</VbenForm>
</VbenDrawer>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useVbenForm, useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { updateWechatUser } from '#/api/core/wechat';
import type { WechatUser, WechatUserForm } from '../data';
interface Props {
modelValue: boolean;
user?: WechatUser | null;
}
interface Emits {
(e: 'update:modelValue', value: boolean): void;
(e: 'success'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const loading = ref(false);
const [VbenForm, formModel, formSchema] = useVbenForm({
schema: [
{
fieldName: 'avatar',
label: '用户信息',
component: 'Slot',
slot: 'avatar',
formItemClass: 'mb-4',
},
{
fieldName: 'remark',
label: '备注',
component: 'InputTextArea',
componentProps: {
placeholder: '请输入备注信息',
rows: 3,
maxlength: 200,
showCount: true,
},
},
{
fieldName: 'groupid',
label: '分组',
component: 'InputNumber',
componentProps: {
placeholder: '请输入分组ID',
min: 0,
max: 100,
},
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
});
const [VbenDrawer, isShow] = useVbenDrawer({
formModel,
formSchema,
});
// 监听props变化
watch(
() => props.modelValue,
(val) => {
isShow.value = val;
if (val && props.user) {
// 初始化表单数据
formModel.value = {
id: props.user.id,
remark: props.user.remark || '',
groupid: props.user.groupid || 0,
headimgurl: props.user.headimgurl,
nickname: props.user.nickname,
openid: props.user.openid,
};
}
},
{ immediate: true },
);
watch(isShow, (val) => {
emit('update:modelValue', val);
});
const handleConfirm = async () => {
try {
loading.value = true;
const data: WechatUserForm = {
id: formModel.value.id,
remark: formModel.value.remark,
groupid: formModel.value.groupid,
};
await updateWechatUser(data);
message.success('用户信息更新成功');
emit('success');
isShow.value = false;
} catch (error) {
message.error('用户信息更新失败');
console.error('更新用户失败:', error);
} finally {
loading.value = false;
}
};
const handleCancel = () => {
isShow.value = false;
};
</script>

View File

@@ -0,0 +1,117 @@
import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface PaymentRecord {
id: number;
order_no: string;
trade_no: string;
user_id: number;
username: string;
amount: string;
pay_type: string;
pay_method: string;
status: 'pending' | 'paid' | 'failed' | 'refunded' | 'closed';
pay_time?: string;
notify_time?: string;
create_time: string;
update_time: string;
}
export interface PaymentForm {
id?: number;
order_no: string;
trade_no: string;
user_id: number;
amount: string;
pay_type: string;
pay_method: string;
status: string;
pay_time?: string;
notify_time?: string;
}
export const payTypeOptions = [
{ label: '微信支付', value: 'wechat' },
{ label: '支付宝', value: 'alipay' },
{ label: '银联', value: 'unionpay' },
{ label: '余额支付', value: 'balance' },
{ label: '其他', value: 'other' },
];
export const statusOptions = [
{ label: '待支付', value: 'pending' },
{ label: '已支付', value: 'paid' },
{ label: '支付失败', value: 'failed' },
{ label: '已退款', value: 'refunded' },
{ label: '已关闭', value: 'closed' },
];
export const statusColorMap = {
pending: 'warning',
paid: 'success',
failed: 'error',
refunded: 'default',
closed: 'default',
};
export const gridOptions: VxeGridProps<PaymentRecord> = {
columns: [
{ type: 'checkbox', width: 50 },
{ field: 'order_no', title: '订单号', minWidth: 180 },
{ field: 'trade_no', title: '交易号', minWidth: 180 },
{ field: 'username', title: '用户', width: 120 },
{ field: 'amount', title: '金额', width: 100, formatter: ({ cellValue }) => {
return `¥${cellValue}`;
}},
{ field: 'pay_type', title: '支付类型', width: 100, formatter: ({ cellValue }) => {
const option = payTypeOptions.find(item => item.value === cellValue);
return option?.label || cellValue;
}},
{ field: 'pay_method', title: '支付方式', width: 120 },
{ field: 'status', title: '状态', width: 100, formatter: ({ cellValue }) => {
const colorMap = {
pending: 'warning',
paid: 'success',
failed: 'error',
refunded: 'default',
closed: 'default',
};
const color = colorMap[cellValue] || 'default';
const option = statusOptions.find(item => item.value === cellValue);
return `<span class="ant-tag ant-tag-${color}">${option?.label || cellValue}</span>`;
} },
{ field: 'pay_time', title: '支付时间', width: 180 },
{ field: 'create_time', title: '创建时间', width: 180 },
{
field: 'action',
fixed: 'right',
title: '操作',
width: 150,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: PaymentRecord) => {
// This will be handled in the component
},
options: [
{ code: 'view', text: '查看详情', icon: 'ant-design:eye-outlined' },
],
},
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
print: true,
refresh: true,
zoom: true,
},
};

View File

@@ -0,0 +1,217 @@
<template>
<Page auto-content-height>
<VbenVxeGrid
ref="gridRef"
:grid-options="gridOptions"
:query-form-schema="queryFormSchema"
@toolbar-button-click="handleToolbarButtonClick"
>
<template #status="{ row }">
<VbenTag :color="statusColorMap[row.status]">
{{ getStatusLabel(row.status) }}
</VbenTag>
</template>
<template #action="{ row }">
<VbenButton
size="small"
type="primary"
variant="text"
@click="handleView(row)"
>
详情
</VbenButton>
<VbenButton
v-if="row.status === 'paid'"
size="small"
type="warning"
variant="text"
@click="handleRefund(row)"
>
退款
</VbenButton>
<VbenPopconfirm
title="确定删除该记录吗?"
@confirm="handleDelete(row)"
>
<VbenButton
size="small"
type="danger"
variant="text"
>
{{ $t('common.delete') }}
</VbenButton>
</VbenPopconfirm>
</template>
</VbenVxeGrid>
<PaymentDetailModal
v-model:visible="detailModalVisible"
:payment-data="viewingPayment"
@cancel="handleDetailModalCancel"
/>
<RefundModal
v-model:visible="refundModalVisible"
:payment-data="refundingPayment"
@cancel="handleRefundModalCancel"
@submit="handleRefundSubmit"
/>
</Page>
</template>
<script lang="ts" setup>
import type { PaymentRecord } from './data';
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenTag, VbenVxeGrid } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { getPaymentListApi, deletePaymentApi, refundPaymentApi } from '#/api/core/finance';
import { SvgIcon } from '#/components/icon';
import PaymentDetailModal from './modules/detail.vue';
import RefundModal from './modules/refund.vue';
import { gridOptions, payTypeOptions, statusOptions, statusColorMap } from './data';
const gridRef = ref();
const detailModalVisible = ref(false);
const refundModalVisible = ref(false);
const viewingPayment = ref<any>(null);
const refundingPayment = ref<any>(null);
const queryFormSchema = computed(() => [
{
component: 'Input',
fieldName: 'order_no',
label: '订单号',
},
{
component: 'Input',
fieldName: 'trade_no',
label: '交易号',
},
{
component: 'Input',
fieldName: 'username',
label: '用户',
},
{
component: 'Select',
fieldName: 'pay_type',
label: '支付类型',
componentProps: {
options: [
{ label: '全部', value: '' },
...payTypeOptions,
],
placeholder: '请选择支付类型',
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
options: [
{ label: '全部', value: '' },
...statusOptions,
],
placeholder: '请选择状态',
},
},
{
component: 'DateRange',
fieldName: 'create_time',
label: '创建时间',
componentProps: {
placeholder: ['开始时间', '结束时间'],
},
},
]);
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
queryFormSchema,
});
function handleToolbarButtonClick(event: string) {
switch (event) {
case 'add':
handleAdd();
break;
case 'refresh':
handleRefresh();
break;
case 'export':
handleExport();
break;
default:
break;
}
}
function handleAdd() {
// Payment records are generated automatically, cannot manually add
$message.info('支付记录自动生成,无法手动添加');
}
function handleView(row: PaymentRecord) {
viewingPayment.value = row;
detailModalVisible.value = true;
}
async function handleRefund(row: PaymentRecord) {
refundingPayment.value = row;
refundModalVisible.value = true;
}
async function handleDelete(row: PaymentRecord) {
try {
await deletePaymentApi(row.id);
await handleRefresh();
$message.success('删除成功');
} catch (error) {
$message.error('删除失败');
}
}
function handleDetailModalCancel() {
detailModalVisible.value = false;
viewingPayment.value = null;
}
function handleRefundModalCancel() {
refundModalVisible.value = false;
refundingPayment.value = null;
}
async function handleRefundSubmit(refundData: any) {
try {
await refundPaymentApi(refundingPayment.value.id, refundData);
refundModalVisible.value = false;
await handleRefresh();
$message.success('退款成功');
} catch (error) {
$message.error('退款失败');
}
}
function getStatusLabel(status: string): string {
const option = statusOptions.find(item => item.value === status);
return option?.label || status;
}
async function handleRefresh() {
await gridApi.query();
}
function handleExport() {
gridApi.exportData({
filename: '支付记录列表',
type: 'csv',
});
}
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium mb-4">支付详情</h3>
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium mb-2">订单信息</h4>
<div class="space-y-2 text-sm">
<div><strong>订单号:</strong> {{ paymentData.order_no }}</div>
<div><strong>交易号:</strong> {{ paymentData.trade_no }}</div>
<div><strong>用户:</strong> {{ paymentData.username }}</div>
<div><strong>金额:</strong> <span class="text-red-600">¥{{ paymentData.amount }}</span></div>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium mb-2">支付信息</h4>
<div class="space-y-2 text-sm">
<div><strong>支付类型:</strong> {{ getPayTypeLabel(paymentData.pay_type) }}</div>
<div><strong>支付方式:</strong> {{ paymentData.pay_method }}</div>
<div><strong>状态:</strong> <VbenTag :color="statusColorMap[paymentData.status]">{{ getStatusLabel(paymentData.status) }}</VbenTag></div>
<div><strong>支付时间:</strong> {{ paymentData.pay_time || '-' }}</div>
</div>
</div>
</div>
</div>
<div class="mb-6">
<h4 class="font-medium mb-2">时间记录</h4>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="grid grid-cols-2 gap-4 text-sm">
<div><strong>创建时间:</strong> {{ paymentData.create_time }}</div>
<div><strong>更新时间:</strong> {{ paymentData.update_time }}</div>
<div><strong>通知时间:</strong> {{ paymentData.notify_time || '-' }}</div>
</div>
</div>
</div>
<div class="flex justify-end space-x-2">
<VbenButton @click="handleClose" variant="outline">
{{ $t('common.close') }}
</VbenButton>
<VbenButton
v-if="paymentData.status === 'paid'"
type="warning"
@click="handleRefund"
>
退款
</VbenButton>
</div>
</div>
</template>
<script lang="ts" setup>
import { VbenButton, VbenTag } from '@vben/common-ui';
import { $t } from '@vben/locale';
interface Props {
paymentData: any;
}
interface Emits {
(e: 'cancel'): void;
(e: 'refund'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const payTypeOptions = [
{ label: '微信支付', value: 'wechat' },
{ label: '支付宝', value: 'alipay' },
{ label: '银联', value: 'unionpay' },
{ label: '余额支付', value: 'balance' },
{ label: '其他', value: 'other' },
];
const statusOptions = [
{ label: '待支付', value: 'pending' },
{ label: '已支付', value: 'paid' },
{ label: '支付失败', value: 'failed' },
{ label: '已退款', value: 'refunded' },
{ label: '已关闭', value: 'closed' },
];
const statusColorMap = {
pending: 'warning',
paid: 'success',
failed: 'error',
refunded: 'default',
closed: 'default',
};
function getPayTypeLabel(type: string): string {
const option = payTypeOptions.find(item => item.value === type);
return option?.label || type;
}
function getStatusLabel(status: string): string {
const option = statusOptions.find(item => item.value === status);
return option?.label || status;
}
function handleClose() {
emit('cancel');
}
function handleRefund() {
emit('refund');
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div class="p-6">
<h3 class="text-lg font-medium mb-4">退款操作</h3>
<VbenForm
:handle-submit="handleSubmit"
:model="model"
:schema="formSchemas"
:show-default-actions="false"
@submit="handleSubmit"
>
<template #form-submit>
<div class="flex items-center justify-end space-x-2">
<VbenButton @click="handleCancel" variant="outline">
{{ $t('common.cancel') }}
</VbenButton>
<VbenButton type="primary" @click="handleSubmit">
确认退款
</VbenButton>
</div>
</template>
</VbenForm>
</div>
</template>
<script lang="ts" setup>
import { VbenButton, VbenForm, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locale';
interface Props {
paymentData: any;
}
interface Emits {
(e: 'cancel'): void;
(e: 'submit', data: any): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const [Modal] = useVbenModal();
const model = ref({
refund_amount: props.paymentData?.amount || '',
refund_reason: '',
notify_url: '',
});
const formSchemas = computed(() => [
{
component: 'InputNumber',
fieldName: 'refund_amount',
label: '退款金额',
rules: 'required|min:0.01|max:' + props.paymentData?.amount,
componentProps: {
placeholder: '请输入退款金额',
min: 0.01,
max: parseFloat(props.paymentData?.amount || '0'),
step: 0.01,
precision: 2,
},
},
{
component: 'Textarea',
fieldName: 'refund_reason',
label: '退款原因',
rules: 'required',
componentProps: {
placeholder: '请输入退款原因',
rows: 3,
maxlength: 200,
showCount: true,
},
},
{
component: 'Input',
fieldName: 'notify_url',
label: '通知地址',
componentProps: {
placeholder: '请输入退款通知地址(可选)',
},
},
]);
async function handleSubmit() {
try {
await Modal?.formApi.validate();
const formValues = Modal?.formApi.getValues() || model.value;
emit('submit', formValues);
} catch (error) {
console.error('Form validation failed:', error);
}
}
function handleCancel() {
emit('cancel');
}
</script>

View File

@@ -0,0 +1,103 @@
import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface AdminLog {
id: number;
admin_id: number;
admin_name: string;
module: string;
controller: string;
action: string;
method: string;
url: string;
params: string;
ip: string;
user_agent: string;
result: 'success' | 'failed';
message?: string;
create_time: string;
}
export interface LogForm {
id?: number;
admin_id: number;
admin_name: string;
module: string;
controller: string;
action: string;
method: string;
url: string;
params: string;
ip: string;
user_agent: string;
result: string;
message?: string;
}
export const resultOptions = [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'failed' },
];
export const methodOptions = [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
{ label: 'PATCH', value: 'PATCH' },
];
export const resultColorMap = {
success: 'success',
failed: 'error',
};
export const gridOptions: VxeGridProps<AdminLog> = {
columns: [
{ type: 'checkbox', width: 50 },
{ field: 'admin_name', title: '管理员', width: 120 },
{ field: 'module', title: '模块', width: 100 },
{ field: 'controller', title: '控制器', width: 120 },
{ field: 'action', title: '操作', width: 100 },
{ field: 'method', title: '方法', width: 80 },
{ field: 'url', title: 'URL', minWidth: 200, showOverflow: true },
{ field: 'ip', title: 'IP地址', width: 120 },
{ field: 'result', title: '结果', width: 80, formatter: ({ cellValue }) => {
const colorMap = { success: 'success', failed: 'error' };
const color = colorMap[cellValue] || 'default';
return `<span class="ant-tag ant-tag-${color}">${cellValue === 'success' ? '成功' : '失败'}</span>`;
} },
{ field: 'create_time', title: '操作时间', width: 180 },
{
field: 'action',
fixed: 'right',
title: '操作',
width: 100,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: AdminLog) => {
// This will be handled in the component
},
options: [
{ code: 'view', text: '查看详情', icon: 'ant-design:eye-outlined' },
],
},
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
print: true,
refresh: true,
zoom: true,
},
};

View File

@@ -0,0 +1,187 @@
<template>
<Page auto-content-height>
<VbenVxeGrid
ref="gridRef"
:grid-options="gridOptions"
:query-form-schema="queryFormSchema"
@toolbar-button-click="handleToolbarButtonClick"
>
<template #toolbar-tools>
<VbenButton type="danger" @click="handleClearLogs">
<SvgIcon icon="mdi:delete-sweep" class="mr-1" />
清空日志
</VbenButton>
</template>
<template #result="{ row }">
<VbenTag :color="resultColorMap[row.result]">
{{ getResultLabel(row.result) }}
</VbenTag>
</template>
<template #action="{ row }">
<VbenButton
size="small"
type="primary"
variant="text"
@click="handleView(row)"
>
详情
</VbenButton>
</template>
</VbenVxeGrid>
<LogDetailModal
v-model:visible="detailModalVisible"
:log-data="viewingLog"
@cancel="handleDetailModalCancel"
/>
</Page>
</template>
<script lang="ts" setup>
import type { AdminLog } from './data';
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid, VbenButton, VbenTag, VbenVxeGrid } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { getAdminLogListApi, deleteAdminLogApi, clearAdminLogApi } from '#/api/core/log';
import { SvgIcon } from '#/components/icon';
import LogDetailModal from './modules/detail.vue';
import { gridOptions, resultOptions, resultColorMap } from './data';
const gridRef = ref();
const detailModalVisible = ref(false);
const viewingLog = ref<any>(null);
const queryFormSchema = computed(() => [
{
component: 'Input',
fieldName: 'admin_name',
label: '管理员',
},
{
component: 'Input',
fieldName: 'module',
label: '模块',
},
{
component: 'Input',
fieldName: 'controller',
label: '控制器',
},
{
component: 'Input',
fieldName: 'action',
label: '操作',
},
{
component: 'Select',
fieldName: 'method',
label: '方法',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
{ label: 'PATCH', value: 'PATCH' },
],
placeholder: '请选择请求方法',
},
},
{
component: 'Input',
fieldName: 'ip',
label: 'IP地址',
},
{
component: 'Select',
fieldName: 'result',
label: '结果',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '成功', value: 'success' },
{ label: '失败', value: 'failed' },
],
placeholder: '请选择操作结果',
},
},
{
component: 'DateRange',
fieldName: 'create_time',
label: '操作时间',
componentProps: {
placeholder: ['开始时间', '结束时间'],
},
},
]);
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
queryFormSchema,
});
function handleToolbarButtonClick(event: string) {
switch (event) {
case 'add':
handleAdd();
break;
case 'refresh':
handleRefresh();
break;
case 'export':
handleExport();
break;
default:
break;
}
}
function handleAdd() {
// Admin logs are generated automatically, cannot manually add
$message.info('管理员日志自动生成,无法手动添加');
}
function handleView(row: AdminLog) {
viewingLog.value = row;
detailModalVisible.value = true;
}
async function handleClearLogs() {
try {
await clearAdminLogApi();
await handleRefresh();
$message.success('日志清空成功');
} catch (error) {
$message.error('日志清空失败');
}
}
function handleDetailModalCancel() {
detailModalVisible.value = false;
viewingLog.value = null;
}
function getResultLabel(result: string): string {
const option = resultOptions.find(item => item.value === result);
return option?.label || result;
}
async function handleRefresh() {
await gridApi.query();
}
function handleExport() {
gridApi.exportData({
filename: '管理员日志列表',
type: 'csv',
});
}
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium mb-4">日志详情</h3>
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium mb-2">管理员信息</h4>
<div class="space-y-2 text-sm">
<div><strong>管理员ID:</strong> {{ logData.admin_id }}</div>
<div><strong>管理员名称:</strong> {{ logData.admin_name }}</div>
<div><strong>IP地址:</strong> {{ logData.ip }}</div>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium mb-2">操作信息</h4>
<div class="space-y-2 text-sm">
<div><strong>模块:</strong> {{ logData.module }}</div>
<div><strong>控制器:</strong> {{ logData.controller }}</div>
<div><strong>操作:</strong> {{ logData.action }}</div>
<div><strong>方法:</strong> {{ logData.method }}</div>
<div><strong>结果:</strong> <VbenTag :color="resultColorMap[logData.result]">{{ getResultLabel(logData.result) }}</VbenTag></div>
</div>
</div>
</div>
</div>
<div class="mb-6">
<h4 class="font-medium mb-2">请求信息</h4>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="space-y-2 text-sm">
<div><strong>URL:</strong> {{ logData.url }}</div>
<div><strong>UserAgent:</strong> {{ logData.user_agent }}</div>
</div>
</div>
</div>
<div class="mb-6">
<h4 class="font-medium mb-2">参数信息</h4>
<div class="bg-gray-50 p-4 rounded-lg">
<pre class="text-sm whitespace-pre-wrap">{{ formatParams(logData.params) }}</pre>
</div>
</div>
<div v-if="logData.message" class="mb-6">
<h4 class="font-medium mb-2">消息</h4>
<div class="bg-gray-50 p-4 rounded-lg">
<pre class="text-sm whitespace-pre-wrap">{{ logData.message }}</pre>
</div>
</div>
<div class="mb-6">
<h4 class="font-medium mb-2">时间记录</h4>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm">
<div><strong>操作时间:</strong> {{ logData.create_time }}</div>
</div>
</div>
</div>
<div class="flex justify-end">
<VbenButton @click="handleClose" variant="outline">
{{ $t('common.close') }}
</VbenButton>
</div>
</div>
</template>
<script lang="ts" setup>
import { VbenButton, VbenTag } from '@vben/common-ui';
import { $t } from '@vben/locale';
interface Props {
logData: any;
}
interface Emits {
(e: 'cancel'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const resultOptions = [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'failed' },
];
const resultColorMap = {
success: 'success',
failed: 'error',
};
function getResultLabel(result: string): string {
const option = resultOptions.find(item => item.value === result);
return option?.label || result;
}
function formatParams(params: string): string {
try {
const parsed = JSON.parse(params);
return JSON.stringify(parsed, null, 2);
} catch {
return params;
}
}
function handleClose() {
emit('cancel');
}
</script>

View File

@@ -87,17 +87,23 @@ export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 50 }, { type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80 }, { field: 'id', title: 'ID', width: 80 },
{ field: 'payment_name', title: '支付名称', minWidth: 150 }, { field: 'payment_name', title: '支付名称', minWidth: 150 },
{ field: 'payment_type', title: '支付类型', width: 120, slots: { default: 'paymentType' } }, { field: 'payment_type', title: '支付类型', width: 120, formatter: ({ cellValue }) => {
return paymentTypeMap[cellValue] || cellValue;
} },
{ field: 'payment_code', title: '支付编码', width: 150 }, { field: 'payment_code', title: '支付编码', width: 150 },
{ {
field: 'icon', field: 'icon',
title: '图标', title: '图标',
width: 100, width: 100,
slots: { default: 'icon' },
align: 'center', align: 'center',
formatter: ({ cellValue }) => {
return cellValue ? `<i class="${cellValue}" style="font-size: 24px;"></i>` : '';
},
}, },
{ field: 'sort', title: '排序', width: 80 }, { field: 'sort', title: '排序', width: 80 },
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } }, { field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
return cellValue === 1 ? '启用' : '禁用';
} },
{ field: 'create_time', title: '创建时间', width: 180 }, { field: 'create_time', title: '创建时间', width: 180 },
{ field: 'update_time', title: '更新时间', width: 180 }, { field: 'update_time', title: '更新时间', width: 180 },
{ {
@@ -105,6 +111,18 @@ export const columns: VxeGridProps['columns'] = [
title: '操作', title: '操作',
width: 150, width: 150,
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: PaymentItem) => {
// This will be handled in the component
},
options: [
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
{ code: 'config', text: '配置', icon: 'ant-design:setting-outlined' },
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
],
},
},
}, },
]; ];

View File

@@ -91,11 +91,15 @@ export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 50 }, { type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80 }, { field: 'id', title: 'ID', width: 80 },
{ field: 'sms_name', title: '短信名称', minWidth: 150 }, { field: 'sms_name', title: '短信名称', minWidth: 150 },
{ field: 'sms_type', title: '短信类型', width: 120, slots: { default: 'smsType' } }, { field: 'sms_type', title: '短信类型', width: 120, formatter: ({ cellValue }) => {
return smsTypeMap[cellValue] || cellValue;
} },
{ field: 'sms_code', title: '短信编码', width: 150 }, { field: 'sms_code', title: '短信编码', width: 150 },
{ field: 'sign_name', title: '签名名称', width: 150 }, { field: 'sign_name', title: '签名名称', width: 150 },
{ field: 'sort', title: '排序', width: 80 }, { field: 'sort', title: '排序', width: 80 },
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } }, { field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
return cellValue === 1 ? '启用' : '禁用';
} },
{ field: 'create_time', title: '创建时间', width: 180 }, { field: 'create_time', title: '创建时间', width: 180 },
{ field: 'update_time', title: '更新时间', width: 180 }, { field: 'update_time', title: '更新时间', width: 180 },
{ {
@@ -103,6 +107,18 @@ export const columns: VxeGridProps['columns'] = [
title: '操作', title: '操作',
width: 150, width: 150,
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: SmsItem) => {
// This will be handled in the component
},
options: [
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
{ code: 'test', text: '测试', icon: 'ant-design:notification-outlined' },
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
],
},
},
}, },
]; ];

View File

@@ -105,11 +105,17 @@ export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 50 }, { type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80 }, { field: 'id', title: 'ID', width: 80 },
{ field: 'storage_name', title: '存储名称', minWidth: 150 }, { field: 'storage_name', title: '存储名称', minWidth: 150 },
{ field: 'storage_type', title: '存储类型', width: 120, slots: { default: 'storageType' } }, { field: 'storage_type', title: '存储类型', width: 120, formatter: ({ cellValue }) => {
return storageTypeMap[cellValue] || cellValue;
} },
{ field: 'storage_code', title: '存储编码', width: 150 }, { field: 'storage_code', title: '存储编码', width: 150 },
{ field: 'is_default', title: '默认存储', width: 100, slots: { default: 'isDefault' } }, { field: 'is_default', title: '默认存储', width: 100, formatter: ({ cellValue }) => {
return cellValue === 1 ? '是' : '否';
} },
{ field: 'sort', title: '排序', width: 80 }, { field: 'sort', title: '排序', width: 80 },
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } }, { field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
return cellValue === 1 ? '启用' : '禁用';
} },
{ field: 'create_time', title: '创建时间', width: 180 }, { field: 'create_time', title: '创建时间', width: 180 },
{ field: 'update_time', title: '更新时间', width: 180 }, { field: 'update_time', title: '更新时间', width: 180 },
{ {
@@ -117,6 +123,18 @@ export const columns: VxeGridProps['columns'] = [
title: '操作', title: '操作',
width: 150, width: 150,
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: StorageItem) => {
// This will be handled in the component
},
options: [
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
{ code: 'setDefault', text: '设为默认', icon: 'ant-design:star-outlined' },
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
],
},
},
}, },
]; ];

View File

@@ -91,12 +91,19 @@ export const querySchema = [
export const columns: VxeGridProps['columns'] = [ export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 50 }, { type: 'checkbox', width: 50 },
{ field: 'id', title: 'ID', width: 80 }, { field: 'id', title: 'ID', width: 80 },
{ field: 'app_module', title: '模块', width: 120, slots: { default: 'module' } }, { field: 'app_module', title: '模块', width: 120, formatter: ({ cellValue }) => {
const option = moduleOptions.find(item => item.value === cellValue);
return option?.label || cellValue;
} },
{ field: 'config_key', title: '配置键', width: 200 }, { field: 'config_key', title: '配置键', width: 200 },
{ field: 'config_value', title: '配置值', minWidth: 200, showOverflow: true }, { field: 'config_value', title: '配置值', minWidth: 200, showOverflow: true },
{ field: 'config_desc', title: '配置描述', minWidth: 200, showOverflow: true }, { field: 'config_desc', title: '配置描述', minWidth: 200, showOverflow: true },
{ field: 'is_system', title: '系统配置', width: 100, slots: { default: 'isSystem' } }, { field: 'is_system', title: '系统配置', width: 100, formatter: ({ cellValue }) => {
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } }, return cellValue === 1 ? '是' : '否';
} },
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
return cellValue === 1 ? '启用' : '禁用';
} },
{ field: 'create_time', title: '创建时间', width: 180 }, { field: 'create_time', title: '创建时间', width: 180 },
{ field: 'update_time', title: '更新时间', width: 180 }, { field: 'update_time', title: '更新时间', width: 180 },
{ {
@@ -104,6 +111,17 @@ export const columns: VxeGridProps['columns'] = [
title: '操作', title: '操作',
width: 150, width: 150,
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: ConfigItem) => {
// This will be handled in the component
},
options: [
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
],
},
},
}, },
]; ];

View File

@@ -0,0 +1,116 @@
import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface SystemConfig {
id: number;
site_id: number;
name: string;
title: string;
value: string;
type: 'text' | 'textarea' | 'number' | 'date' | 'datetime' | 'select' | 'radio' | 'checkbox' | 'image' | 'file' | 'color' | 'array' | 'json';
options?: string;
tips?: string;
group: string;
sort: number;
status: 0 | 1;
create_time: string;
update_time: string;
}
export interface ConfigForm {
id?: number;
site_id: number;
name: string;
title: string;
value: string;
type: string;
options?: string;
tips?: string;
group: string;
sort: number;
status: 0 | 1;
}
export const typeOptions = [
{ label: '文本框', value: 'text' },
{ label: '文本域', value: 'textarea' },
{ label: '数字', value: 'number' },
{ label: '日期', value: 'date' },
{ label: '日期时间', value: 'datetime' },
{ label: '下拉框', value: 'select' },
{ label: '单选框', value: 'radio' },
{ label: '复选框', value: 'checkbox' },
{ label: '图片上传', value: 'image' },
{ label: '文件上传', value: 'file' },
{ label: '颜色选择', value: 'color' },
{ label: '数组', value: 'array' },
{ label: 'JSON', value: 'json' },
];
export const statusOptions = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
];
export const groupOptions = [
{ label: '站点配置', value: 'site' },
{ label: '系统配置', value: 'system' },
{ label: '上传配置', value: 'upload' },
{ label: '邮件配置', value: 'email' },
{ label: '短信配置', value: 'sms' },
{ label: '支付配置', value: 'payment' },
{ label: '其他配置', value: 'other' },
];
export const gridOptions: VxeGridProps<SystemConfig> = {
columns: [
{ type: 'checkbox', width: 50 },
{ field: 'name', title: '配置名称', minWidth: 150 },
{ field: 'title', title: '配置标题', minWidth: 150 },
{ field: 'type', title: '类型', width: 100, formatter: ({ cellValue }) => {
const option = typeOptions.find(item => item.value === cellValue);
return option?.label || cellValue;
}},
{ field: 'group', title: '分组', width: 100, formatter: ({ cellValue }) => {
const option = groupOptions.find(item => item.value === cellValue);
return option?.label || cellValue;
}},
{ field: 'value', title: '配置值', minWidth: 200, showOverflow: true },
{ field: 'sort', title: '排序', width: 80 },
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
return cellValue === 1 ? '启用' : '禁用';
}},
{
field: 'action',
fixed: 'right',
title: '操作',
width: 150,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: SystemConfig) => {
// This will be handled in the component
},
options: [
{ code: 'edit', text: '编辑', icon: 'ant-design:edit-outlined' },
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
],
},
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
print: true,
refresh: true,
zoom: true,
},
};

View File

@@ -0,0 +1,204 @@
<template>
<Page auto-content-height>
<VbenVxeGrid
ref="gridRef"
:grid-options="gridOptions"
:query-form-schema="queryFormSchema"
@toolbar-button-click="handleToolbarButtonClick"
>
<template #toolbar-tools>
<VbenButton type="primary" @click="handleRefreshCache">
<SvgIcon icon="mdi:refresh" class="mr-1" />
刷新缓存
</VbenButton>
</template>
<template #action="{ row }">
<VbenButton
size="small"
type="primary"
variant="text"
@click="handleEdit(row)"
>
{{ $t('common.edit') }}
</VbenButton>
<VbenPopconfirm
title="确定删除该配置吗?"
@confirm="handleDelete(row)"
>
<VbenButton
size="small"
type="danger"
variant="text"
>
{{ $t('common.delete') }}
</VbenButton>
</VbenPopconfirm>
</template>
</VbenVxeGrid>
<ConfigFormModal
v-model:visible="modalVisible"
:id="editingId"
@cancel="handleModalCancel"
@submit="handleModalSubmit"
/>
</Page>
</template>
<script lang="ts" setup>
import type { SystemConfig, ConfigForm } from './data';
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenVxeGrid } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { getSystemConfigListApi, createSystemConfigApi, updateSystemConfigApi, deleteSystemConfigApi, refreshSystemConfigCacheApi } from '#/api/core/system';
import { SvgIcon } from '#/components/icon';
import ConfigFormModal from './modules/form.vue';
import { gridOptions } from './data';
const gridRef = ref();
const modalVisible = ref(false);
const editingId = ref<number | undefined>();
const queryFormSchema = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '配置名称',
},
{
component: 'Input',
fieldName: 'title',
label: '配置标题',
},
{
component: 'Select',
fieldName: 'group',
label: '配置分组',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '站点配置', value: 'site' },
{ label: '系统配置', value: 'system' },
{ label: '上传配置', value: 'upload' },
{ label: '邮件配置', value: 'email' },
{ label: '短信配置', value: 'sms' },
{ label: '支付配置', value: 'payment' },
{ label: '其他配置', value: 'other' },
],
placeholder: '请选择配置分组',
},
},
{
component: 'Select',
fieldName: 'type',
label: '配置类型',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '文本框', value: 'text' },
{ label: '文本域', value: 'textarea' },
{ label: '数字', value: 'number' },
{ label: '日期', value: 'date' },
{ label: '日期时间', value: 'datetime' },
{ label: '下拉框', value: 'select' },
{ label: '单选框', value: 'radio' },
{ label: '复选框', value: 'checkbox' },
{ label: '图片上传', value: 'image' },
{ label: '文件上传', value: 'file' },
{ label: '颜色选择', value: 'color' },
{ label: '数组', value: 'array' },
{ label: 'JSON', value: 'json' },
],
placeholder: '请选择配置类型',
},
},
]);
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
queryFormSchema,
});
function handleToolbarButtonClick(event: string) {
switch (event) {
case 'add':
handleAdd();
break;
case 'refresh':
handleRefresh();
break;
case 'export':
handleExport();
break;
default:
break;
}
}
function handleAdd() {
editingId.value = undefined;
modalVisible.value = true;
}
function handleEdit(row: SystemConfig) {
editingId.value = row.id;
modalVisible.value = true;
}
async function handleDelete(row: SystemConfig) {
try {
await deleteSystemConfigApi(row.id);
await handleRefresh();
$message.success('删除成功');
} catch (error) {
$message.error('删除失败');
}
}
async function handleRefreshCache() {
try {
await refreshSystemConfigCacheApi();
$message.success('缓存刷新成功');
} catch (error) {
$message.error('缓存刷新失败');
}
}
function handleModalCancel() {
modalVisible.value = false;
editingId.value = undefined;
}
async function handleModalSubmit(data: ConfigForm) {
try {
if (editingId.value) {
await updateSystemConfigApi(editingId.value, data);
$message.success('更新成功');
} else {
await createSystemConfigApi(data);
$message.success('创建成功');
}
modalVisible.value = false;
await handleRefresh();
} catch (error) {
$message.error(editingId.value ? '更新失败' : '创建失败');
}
}
async function handleRefresh() {
await gridApi.query();
}
function handleExport() {
gridApi.exportData({
filename: '系统配置列表',
type: 'csv',
});
}
</script>

View File

@@ -0,0 +1,87 @@
<template>
<div>
<VbenForm
:handle-submit="handleSubmit"
:model="model"
:schema="formSchemas"
:show-default-actions="false"
@submit="handleSubmit"
>
<template #form-submit>
<div class="flex items-center justify-end space-x-2">
<VbenButton @click="handleCancel" variant="outline">
{{ $t('common.cancel') }}
</VbenButton>
<VbenButton type="primary" @click="handleSubmit">
{{ $t('common.confirm') }}
</VbenButton>
</div>
</template>
</VbenForm>
</div>
</template>
<script lang="ts" setup>
import type { ConfigForm } from '../data';
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { useConfigFormSchemas } from './formSchemas';
interface Props {
id?: number;
}
interface Emits {
(e: 'submit', data: ConfigForm): void;
(e: 'cancel'): void;
}
const props = withDefaults(defineProps<Props>(), {
id: undefined,
});
const emit = defineEmits<Emits>();
const [Drawer] = useVbenDrawer();
const model = ref<ConfigForm>({
site_id: 0,
name: '',
title: '',
type: 'text',
value: '',
group: 'other',
sort: 0,
status: 1,
});
const formSchemas = useConfigFormSchemas();
async function handleSubmit() {
try {
await Drawer?.formApi.validate();
const formValues = Drawer?.formApi.getValues() || model.value;
emit('submit', formValues);
} catch (error) {
console.error('Form validation failed:', error);
}
}
function handleCancel() {
emit('cancel');
}
// Load config data if editing
onMounted(async () => {
if (props.id) {
try {
// Load config data
const configData = await getSystemConfigDetailApi(props.id);
model.value = { ...configData };
} catch (error) {
console.error('Failed to load config data:', error);
}
}
});
</script>

View File

@@ -0,0 +1,92 @@
import type { ConfigForm } from '../data';
import { useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { typeOptions, statusOptions, groupOptions } from '../data';
export const useConfigFormSchemas = () => {
const formSchemas = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '配置名称',
rules: 'required|pattern:^[a-zA-Z_][a-zA-Z0-9_]*$',
componentProps: {
placeholder: '请输入配置名称(英文)',
},
},
{
component: 'Input',
fieldName: 'title',
label: '配置标题',
rules: 'required',
componentProps: {
placeholder: '请输入配置标题',
},
},
{
component: 'Select',
fieldName: 'type',
label: '配置类型',
rules: 'required',
componentProps: {
options: typeOptions,
placeholder: '请选择配置类型',
},
},
{
component: 'Select',
fieldName: 'group',
label: '配置分组',
rules: 'required',
componentProps: {
options: groupOptions,
placeholder: '请选择配置分组',
},
},
{
component: 'Textarea',
fieldName: 'options',
label: '配置选项',
ifShow: computed(() => {
const form = useVbenForm().getValues();
return ['select', 'radio', 'checkbox'].includes(form.type);
}),
componentProps: {
placeholder: '请输入配置选项格式key:value每行一个',
rows: 4,
},
},
{
component: 'Textarea',
fieldName: 'tips',
label: '配置说明',
componentProps: {
placeholder: '请输入配置说明',
rows: 3,
},
},
{
component: 'InputNumber',
fieldName: 'sort',
label: '排序',
defaultValue: 0,
componentProps: {
min: 0,
max: 999,
},
},
{
component: 'RadioGroup',
fieldName: 'status',
label: '状态',
defaultValue: 1,
componentProps: {
options: statusOptions,
},
},
]);
return formSchemas;
};

View File

@@ -195,77 +195,47 @@ export function useColumns(
title: $t('site.list.status'), title: $t('site.list.status'),
field: 'status', field: 'status',
width: 100, width: 100,
slots: { formatter: ({ row }) => {
default: ({ row }) => ( const colorMap: Record<number, string> = {
<Tag 0: 'red',
color={getStatusColor(row.status)} 1: 'green',
class="cursor-pointer" 2: 'orange',
onClick={() => onStatusChange(row.status, row.site_id)} 3: 'red',
> };
{row.status_name} const color = colorMap[row.status] || 'default';
</Tag> return `<span class="ant-tag ant-tag-${color} cursor-pointer" onclick="window.handleStatusChange(${row.status}, ${row.site_id})">${row.status_name}</span>`;
),
}, },
}, },
{ {
title: $t('common.action'), title: $t('common.action'),
field: 'action', field: 'action',
width: 280, width: 280,
slots: { cellRender: {
default: ({ row }) => ( name: 'CellOperation',
<Space> attrs: {
<Button onClick: (code: string, row: any) => {
type="link" if (code === 'toSite') {
size="small" window.open(`/site/${row.site_id}`, '_blank');
onClick={() => handleToSiteLink(row.site_id)} } else if (code === 'toggleStatus') {
> onOpenClose(row.status, row.site_id);
<Icon icon="ant-design:login-outlined" /> } else if (code === 'info') {
{$t('site.list.toSite')} onActionClick('info', row);
</Button> } else if (code === 'init') {
{(row.status === 1 || row.status === 3) && ( onActionClick('init', row);
<Button } else if (code === 'edit') {
type="link" onActionClick('edit', row);
size="small" } else if (code === 'delete') {
onClick={() => onOpenClose(row.status, row.site_id)} onActionClick('delete', row);
> }
{row.status === 1 ? $t('site.list.close') : $t('site.list.open')} },
</Button> options: [
)} { code: 'toSite', text: $t('site.list.toSite'), icon: 'ant-design:login-outlined' },
<Button { code: 'info', text: $t('site.list.info'), icon: 'ant-design:info-circle-outlined' },
type="link" { code: 'init', text: $t('site.list.initSite'), icon: 'ant-design:reload-outlined' },
size="small" { code: 'edit', text: $t('common.edit'), icon: 'ant-design:edit-outlined' },
onClick={() => onActionClick('info', row)} { code: 'delete', text: $t('common.delete'), icon: 'ant-design:delete-outlined', danger: true },
> ],
<Icon icon="ant-design:info-circle-outlined" /> },
{$t('site.list.info')}
</Button>
<Button
type="link"
size="small"
onClick={() => onActionClick('init', row)}
>
<Icon icon="ant-design:reload-outlined" />
{$t('site.list.initSite')}
</Button>
<Button
type="link"
size="small"
onClick={() => onActionClick('edit', row)}
>
<Icon icon="ant-design:edit-outlined" />
{$t('common.edit')}
</Button>
<Popconfirm
title={$t('site.list.deleteConfirm')}
onConfirm={() => onActionClick('delete', row)}
>
<Button type="link" size="small" danger>
<Icon icon="ant-design:delete-outlined" />
{$t('common.delete')}
</Button>
</Popconfirm>
</Space>
),
}, },
}, },
]; ];

View File

@@ -29,10 +29,12 @@ export function useColumns(
align: 'left', align: 'left',
field: 'meta.title', field: 'meta.title',
fixed: 'left', fixed: 'left',
slots: { default: 'title' },
title: $t('system.menu.menuTitle'), title: $t('system.menu.menuTitle'),
treeNode: true, treeNode: true,
width: 250, width: 250,
formatter: ({ row }) => {
return row.meta?.title || '';
},
}, },
{ {
align: 'center', align: 'center',

View File

@@ -0,0 +1,99 @@
import type { VxeGridProps } from '@vben/plugins/vxe-table';
export interface BackupInfo {
id: number;
name: string;
type: 'database' | 'file' | 'full';
size: string;
path: string;
status: 'success' | 'failed' | 'running';
start_time: string;
end_time?: string;
create_time: string;
}
export interface BackupForm {
id?: number;
name: string;
type: string;
tables?: string[];
exclude_tables?: string[];
compress: 0 | 1;
}
export const typeOptions = [
{ label: '数据库备份', value: 'database' },
{ label: '文件备份', value: 'file' },
{ label: '完整备份', value: 'full' },
];
export const statusOptions = [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'failed' },
{ label: '进行中', value: 'running' },
];
export const statusColorMap = {
success: 'success',
failed: 'error',
running: 'processing',
};
export const gridOptions: VxeGridProps<BackupInfo> = {
columns: [
{ type: 'checkbox', width: 50 },
{ field: 'name', title: '备份名称', minWidth: 150 },
{ field: 'type', title: '备份类型', width: 120, formatter: ({ cellValue }) => {
const option = typeOptions.find(item => item.value === cellValue);
return option?.label || cellValue;
}},
{ field: 'size', title: '文件大小', width: 100 },
{ field: 'status', title: '状态', width: 100, formatter: ({ cellValue }) => {
const colorMap = {
success: 'success',
failed: 'error',
running: 'processing',
};
const color = colorMap[cellValue] || 'default';
const option = statusOptions.find(item => item.value === cellValue);
return `<span class="ant-tag ant-tag-${color}">${option?.label || cellValue}</span>`;
} },
{ field: 'start_time', title: '开始时间', width: 180 },
{ field: 'end_time', title: '结束时间', width: 180 },
{ field: 'create_time', title: '创建时间', width: 180 },
{
field: 'action',
fixed: 'right',
title: '操作',
width: 200,
cellRender: {
name: 'CellOperation',
attrs: {
onClick: (code: string, row: BackupInfo) => {
// This will be handled in the component
},
options: [
{ code: 'download', text: '下载', icon: 'ant-design:download-outlined' },
{ code: 'restore', text: '还原', icon: 'ant-design:reload-outlined' },
{ code: 'delete', text: '删除', icon: 'ant-design:delete-outlined', danger: true },
],
},
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
print: true,
refresh: true,
zoom: true,
},
};

View File

@@ -0,0 +1,219 @@
<template>
<Page auto-content-height>
<VbenVxeGrid
ref="gridRef"
:grid-options="gridOptions"
:query-form-schema="queryFormSchema"
@toolbar-button-click="handleToolbarButtonClick"
>
<template #toolbar-tools>
<VbenButton type="primary" @click="handleCreateBackup">
<SvgIcon icon="mdi:backup" class="mr-1" />
创建备份
</VbenButton>
</template>
<template #status="{ row }">
<VbenTag :color="statusColorMap[row.status]">
{{ getStatusLabel(row.status) }}
</VbenTag>
</template>
<template #action="{ row }">
<VbenButton
size="small"
type="primary"
variant="text"
@click="handleDownload(row)"
>
下载
</VbenButton>
<VbenButton
size="small"
type="primary"
variant="text"
@click="handleRestore(row)"
>
还原
</VbenButton>
<VbenPopconfirm
title="确定删除该备份吗?"
@confirm="handleDelete(row)"
>
<VbenButton
size="small"
type="danger"
variant="text"
>
{{ $t('common.delete') }}
</VbenButton>
</VbenPopconfirm>
</template>
</VbenVxeGrid>
<BackupFormModal
v-model:visible="modalVisible"
:id="editingId"
@cancel="handleModalCancel"
@submit="handleModalSubmit"
/>
</Page>
</template>
<script lang="ts" setup>
import type { BackupInfo, BackupForm } from './data';
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid, VbenButton, VbenPopconfirm, VbenTag, VbenVxeGrid } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { getBackupListApi, createBackupApi, deleteBackupApi, downloadBackupApi, restoreBackupApi } from '#/api/core/tools';
import { SvgIcon } from '#/components/icon';
import BackupFormModal from './modules/form.vue';
import { gridOptions, statusOptions, statusColorMap } from './data';
const gridRef = ref();
const modalVisible = ref(false);
const editingId = ref<number | undefined>();
const queryFormSchema = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '备份名称',
},
{
component: 'Select',
fieldName: 'type',
label: '备份类型',
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '数据库备份', value: 'database' },
{ label: '文件备份', value: 'file' },
{ label: '完整备份', value: 'full' },
],
placeholder: '请选择备份类型',
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
options: [
{ label: '全部', value: '' },
...statusOptions,
],
placeholder: '请选择状态',
},
},
{
component: 'DateRange',
fieldName: 'create_time',
label: '创建时间',
componentProps: {
placeholder: ['开始时间', '结束时间'],
},
},
]);
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
queryFormSchema,
});
function handleToolbarButtonClick(event: string) {
switch (event) {
case 'add':
handleAdd();
break;
case 'refresh':
handleRefresh();
break;
case 'export':
handleExport();
break;
default:
break;
}
}
function handleAdd() {
editingId.value = undefined;
modalVisible.value = true;
}
function getStatusLabel(status: string): string {
const option = statusOptions.find(item => item.value === status);
return option?.label || status;
}
async function handleDownload(row: BackupInfo) {
try {
await downloadBackupApi(row.id);
$message.success('下载开始');
} catch (error) {
$message.error('下载失败');
}
}
async function handleRestore(row: BackupInfo) {
try {
await restoreBackupApi(row.id);
await handleRefresh();
$message.success('还原成功');
} catch (error) {
$message.error('还原失败');
}
}
async function handleDelete(row: BackupInfo) {
try {
await deleteBackupApi(row.id);
await handleRefresh();
$message.success('删除成功');
} catch (error) {
$message.error('删除失败');
}
}
function handleModalCancel() {
modalVisible.value = false;
editingId.value = undefined;
}
async function handleModalSubmit(data: BackupForm) {
try {
if (editingId.value) {
// Backup editing is not supported, only creation
$message.info('备份不支持编辑');
} else {
await createBackupApi(data);
$message.success('备份创建成功');
}
modalVisible.value = false;
await handleRefresh();
} catch (error) {
$message.error(editingId.value ? '操作失败' : '创建失败');
}
}
async function handleRefresh() {
await gridApi.query();
}
function handleExport() {
gridApi.exportData({
filename: '备份列表',
type: 'csv',
});
}
function handleCreateBackup() {
handleAdd();
}
</script>

View File

@@ -0,0 +1,82 @@
<template>
<div>
<VbenForm
:handle-submit="handleSubmit"
:model="model"
:schema="formSchemas"
:show-default-actions="false"
@submit="handleSubmit"
>
<template #form-submit>
<div class="flex items-center justify-end space-x-2">
<VbenButton @click="handleCancel" variant="outline">
{{ $t('common.cancel') }}
</VbenButton>
<VbenButton type="primary" @click="handleSubmit">
{{ $t('common.confirm') }}
</VbenButton>
</div>
</template>
</VbenForm>
</div>
</template>
<script lang="ts" setup>
import type { BackupForm } from '../data';
import { VbenButton, VbenForm, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { useBackupFormSchemas } from './formSchemas';
interface Props {
id?: number;
}
interface Emits {
(e: 'submit', data: BackupForm): void;
(e: 'cancel'): void;
}
const props = withDefaults(defineProps<Props>(), {
id: undefined,
});
const emit = defineEmits<Emits>();
const [Drawer] = useVbenDrawer();
const model = ref<BackupForm>({
name: '',
type: 'database',
compress: 1,
});
const formSchemas = useBackupFormSchemas();
async function handleSubmit() {
try {
await Drawer?.formApi.validate();
const formValues = Drawer?.formApi.getValues() || model.value;
emit('submit', formValues);
} catch (error) {
console.error('Form validation failed:', error);
}
}
function handleCancel() {
emit('cancel');
}
// Load backup data if editing
onMounted(async () => {
if (props.id) {
try {
// Load backup data
const backupData = await getBackupDetailApi(props.id);
model.value = { ...backupData };
} catch (error) {
console.error('Failed to load backup data:', error);
}
}
});
</script>

View File

@@ -0,0 +1,70 @@
import type { BackupForm } from '../data';
import { useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locale';
import { typeOptions } from '../data';
export const useBackupFormSchemas = () => {
const formSchemas = computed(() => [
{
component: 'Input',
fieldName: 'name',
label: '备份名称',
rules: 'required',
componentProps: {
placeholder: '请输入备份名称',
},
},
{
component: 'Select',
fieldName: 'type',
label: '备份类型',
rules: 'required',
componentProps: {
options: typeOptions,
placeholder: '请选择备份类型',
},
},
{
component: 'Select',
fieldName: 'tables',
label: '备份表',
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'database';
}),
componentProps: {
mode: 'multiple',
placeholder: '请选择要备份的数据表',
options: [], // This should be populated with actual table list
},
},
{
component: 'Select',
fieldName: 'exclude_tables',
label: '排除表',
ifShow: computed(() => {
const form = useVbenForm().getValues();
return form.type === 'database';
}),
componentProps: {
mode: 'multiple',
placeholder: '请选择要排除的数据表',
options: [], // This should be populated with actual table list
},
},
{
component: 'Switch',
fieldName: 'compress',
label: '压缩备份',
defaultValue: 1,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
},
]);
return formSchemas;
};

View File

@@ -263,6 +263,7 @@ const notificationEl : any = null
const elNotificationClick = () => { const elNotificationClick = () => {
showDialog.value = true showDialog.value = true
cloudBuildModalApi.open()
active.value = 'build' active.value = 'build'
getCloudBuildLogFn() getCloudBuildLogFn()
} }
@@ -284,18 +285,22 @@ const open = async () => {
loading.value = false loading.value = false
cloudBuildTask.value = data cloudBuildTask.value = data
showDialog.value = true showDialog.value = true
cloudBuildModalApi.open()
getCloudBuildLogFn() getCloudBuildLogFn()
}).catch(() => { }).catch(() => {
showDialog.value = false showDialog.value = false
cloudBuildModalApi.close()
loading.value = false loading.value = false
}) })
} else { } else {
loading.value = false loading.value = false
cloudBuildCheck.value = data cloudBuildCheck.value = data
showDialog.value = true showDialog.value = true
cloudBuildModalApi.open()
} }
}).catch(() => { }).catch(() => {
showDialog.value = false showDialog.value = false
cloudBuildModalApi.close()
}) })
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="dialogVisible" :title="t('gxx')" width="850" :destroy-on-close="true"> <ModalUpgradeLog :class="'w-[850px] time-dialog'" :title="t('gxx')">
<el-card class="box-card !border-none" shadow="never" > <el-card class="box-card !border-none" shadow="never" >
<div v-loading="loading"> <div v-loading="loading">
<div class="text-page-title mb-[20px]">历史版本</div> <div class="text-page-title mb-[20px]">历史版本</div>
@@ -28,12 +28,13 @@
</div> </div>
</div> </div>
</el-card> </el-card>
</el-dialog> </ModalUpgradeLog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, defineProps, nextTick } from "vue" import { computed, ref, defineProps, nextTick } from "vue"
import { t } from "@/lang" import { t } from "@/lang"
import { getAppVersionList, getFrameworkVersionList } from "@/app/api/module" import { getAppVersionList, getFrameworkVersionList } from "@/app/api/module"
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({ const props = defineProps({
upgradeKey: { upgradeKey: {
@@ -65,6 +66,7 @@ const getAppVersionListFn = () => {
return return
}else{ }else{
dialogVisible.value = true dialogVisible.value = true
upgradeLogModalApi.open()
} }
}) })
} }
@@ -80,6 +82,7 @@ const getFrameworkVersionListFn = () => {
}) })
frameworkVersionList.value = data frameworkVersionList.value = data
dialogVisible.value = true dialogVisible.value = true
upgradeLogModalApi.open()
}) })
} }
@@ -88,6 +91,7 @@ const activeName = ref(0)
// 提交信息 // 提交信息
const loading = ref(true) const loading = ref(true)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const [ModalUpgradeLog, upgradeLogModalApi] = useVbenModal()
const open = async () => { const open = async () => {
nextTick(() => { nextTick(() => {
activeName.value = 0 // 重置 activeName.value = 0 // 重置

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('upgrade.title')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="dialogClose"> <ModalUpgrade :class="'w-[850px]'" :title="t('upgrade.title')" :close-on-click-modal="false" :close-on-press-escape="false" @close="dialogClose">
<template v-if="upgradeContent"> <template v-if="upgradeContent">
<!-- 检测服务是否到期 --> <!-- 检测服务是否到期 -->
@@ -203,9 +203,9 @@
</template> </template>
</div> </div>
</template> </template>
</el-dialog> </ModalUpgrade>
<el-dialog v-model="upgradeTipsShowDialog" :title="t('warning')" width="500px" draggable> <ModalUpgradeTips :class="'w-[500px]'" :title="t('warning')">
<span v-html="t('upgrade.upgradeTips')"></span> <span v-html="t('upgrade.upgradeTips')"></span>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
@@ -214,9 +214,9 @@
<el-button @click="upgradeTipsShowDialog = false">{{ t("cancel") }}</el-button> <el-button @click="upgradeTipsShowDialog = false">{{ t("cancel") }}</el-button>
</div> </div>
</template> </template>
</el-dialog> </ModalUpgradeTips>
<el-dialog v-model="cloudBuildErrorTipsShowDialog" :title="t('warning')" width="500px" draggable :show-close="false"> <ModalCloudBuildError :class="'w-[500px]'" :title="t('warning')">
<span v-html="t('upgrade.cloudBuildErrorTips')"></span> <span v-html="t('upgrade.cloudBuildErrorTips')"></span>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
@@ -225,7 +225,7 @@
<el-button @click="cloudBuildError('rollback')" type="primary">{{ t("upgrade.rollback") }}</el-button> <el-button @click="cloudBuildError('rollback')" type="primary">{{ t("upgrade.rollback") }}</el-button>
</div> </div>
</template> </template>
</el-dialog> </ModalCloudBuildError>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -544,6 +544,7 @@ const open = (addonKey: string = '', callback = null) => {
handleUpgrade() handleUpgrade()
} else { } else {
upgradeTipsShowDialog.value = true upgradeTipsShowDialog.value = true
upgradeTipsModalApi.open()
} }
if (callback) callback() if (callback) callback()
}).catch(() => { }).catch(() => {
@@ -631,6 +632,7 @@ const clearUpgradeTaskFn = () => {
*/ */
const cloudBuildError = (event: string) => { const cloudBuildError = (event: string) => {
cloudBuildErrorTipsShowDialog.value = false cloudBuildErrorTipsShowDialog.value = false
cloudErrorModalApi.close()
switch (event) { switch (event) {
case 'local': case 'local':
upgradeUserOperate(event).then(() => { upgradeUserOperate(event).then(() => {
@@ -658,7 +660,11 @@ const timeSplit = (str: string) => {
const upgradeTipsConfirm = (isLock: boolean = false) => { const upgradeTipsConfirm = (isLock: boolean = false) => {
isLock && Storage.set({ key: 'upgradeTipsLock', data: isLock }) isLock && Storage.set({ key: 'upgradeTipsLock', data: isLock })
upgradeTipsShowDialog.value = false upgradeTipsShowDialog.value = false
!isLock && (showDialog.value = true) upgradeTipsModalApi.close()
if (!isLock) {
showDialog.value = true
upgradeModalApi.open()
}
} }
const activeName = ref(0) const activeName = ref(0)
const numberOfSteps = ref(0) const numberOfSteps = ref(0)

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="dialogVisible" :title="t('accountSettings')" width="500"> <ModalUserInfoEdit :class="'w-[500px]'" :title="t('accountSettings')">
<el-form :model="saveInfo" label-width="90px" ref="formRef" class="page-form"> <el-form :model="saveInfo" label-width="90px" ref="formRef" class="page-form">
<el-form-item :label="t('headImg')"> <el-form-item :label="t('headImg')">
<upload-image v-model="saveInfo.head_img" :limit="1" imageFit="cover" /> <upload-image v-model="saveInfo.head_img" :limit="1" imageFit="cover" />
@@ -14,10 +14,10 @@
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
<el-button type="primary" @click="submitForm(formRef)" :loading="loading">{{ t('save') }}</el-button> <el-button type="primary" @click="submitForm(formRef)" :loading="loading">{{ t('save') }}</el-button>
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
</div> </div>
</template> </template>
</el-dialog> </ModalUserInfoEdit>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
@@ -26,6 +26,7 @@ import type { FormInstance } from 'element-plus'
import { deepClone } from '@/utils/common' import { deepClone } from '@/utils/common'
import { getUserInfo, setUserInfo } from '@/app/api/personal' import { getUserInfo, setUserInfo } from '@/app/api/personal'
import useUserStore from '@/stores/modules/user' import useUserStore from '@/stores/modules/user'
import { useVbenModal } from '@vben/common-ui'
const userStore = useUserStore() const userStore = useUserStore()
// 提交信息 // 提交信息
@@ -33,6 +34,7 @@ const saveInfo: any = reactive({})
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const loading = ref(true) const loading = ref(true)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const [ModalUserInfoEdit, userInfoModalApi] = useVbenModal()
/** /**
* 获取用户信息 * 获取用户信息
*/ */
@@ -48,6 +50,7 @@ const getUserInfoFn = () => {
getUserInfoFn() getUserInfoFn()
const open = ()=>{ const open = ()=>{
dialogVisible.value = true dialogVisible.value = true
userInfoModalApi.open()
getUserInfoFn() getUserInfoFn()
} }
const submitForm = (formEl: FormInstance | undefined) => { const submitForm = (formEl: FormInstance | undefined) => {
@@ -58,6 +61,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
setUserInfo(saveInfo).then((res: any) => { setUserInfo(saveInfo).then((res: any) => {
loading.value = false loading.value = false
dialogVisible.value = false dialogVisible.value = false
userInfoModalApi.close()
let data: any = deepClone(userStore.userInfo) let data: any = deepClone(userStore.userInfo)
data.head_img = saveInfo.head_img data.head_img = saveInfo.head_img
userStore.setUserInfo(data) userStore.setUserInfo(data)
@@ -69,6 +73,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
} }
}) })
} }
const cancel = () => { dialogVisible.value = false; userInfoModalApi.close() }
defineExpose({ defineExpose({
open open
}) })

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true"> <ModalRole :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('roleName')" prop="role_name"> <el-form-item :label="t('roleName')" prop="role_name">
<el-input v-model.trim="formData.role_name" :placeholder="t('roleNamePlaceholder')" clearable :disabled="formData.uid" class="input-width" maxlength="10" :show-word-limit="true" /> <el-input v-model.trim="formData.role_name" :placeholder="t('roleNamePlaceholder')" clearable :disabled="formData.uid" class="input-width" maxlength="10" :show-word-limit="true" />
@@ -29,11 +29,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalRole>
</template> </template>
<script lang="ts" setup async> <script lang="ts" setup async>
@@ -43,7 +43,7 @@ import type { FormInstance } from 'element-plus'
import { addRole, editRole, getRoleInfo, getSiteMenus } from '@/app/api/sys' import { addRole, editRole, getRoleInfo, getSiteMenus } from '@/app/api/sys'
import { debounce } from '@/utils/common' import { debounce } from '@/utils/common'
const showDialog = ref(false) const [ModalRole, modalApi] = useVbenModal()
const loading = ref(false) const loading = ref(false)
const isOpen = ref(true) const isOpen = ref(true)
@@ -148,7 +148,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
save(data).then(res => { save(data).then(res => {
loading.value = false loading.value = false
showDialog.value = false modalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
@@ -184,6 +184,7 @@ const setFormData = async (row: any = null) => {
}) })
} }
loading.value = false loading.value = false
modalApi.open()
} }
function checked (menuKey:string, data:any, newArr:any) { function checked (menuKey:string, data:any, newArr:any) {
@@ -201,10 +202,9 @@ function checked (menuKey:string, data:any, newArr:any) {
}) })
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true"> <ModalUser :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<!-- <el-form-item :label="t('accountNumber')" v-if="!formData.uid" prop="uid"> <!-- <el-form-item :label="t('accountNumber')" v-if="!formData.uid" prop="uid">
@@ -53,11 +53,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalUser>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -95,7 +95,7 @@ const needAddUserInfo = computed(() => {
} }
}) })
const showDialog = ref(false) const [ModalUser, modalApi] = useVbenModal()
const loading = ref(false) const loading = ref(false)
let popTitle: string = '' let popTitle: string = ''
@@ -181,7 +181,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
if (!formData.uid && typeof uid.value == 'number') data.uid = uid.value if (!formData.uid && typeof uid.value == 'number') data.uid = uid.value
save(data).then(res => { save(data).then(res => {
loading.value = false loading.value = false
showDialog.value = false modalApi.close()
!formData.uid && getUserList() !formData.uid && getUserList()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
@@ -206,12 +206,12 @@ const setFormData = async (row: any = null) => {
}) })
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="formData.id ? t('updateAppVersion') : t('addAppVersion')" width="60%" class="diy-dialog-wrap" :destroy-on-close="true"> <ModalAppVersionEdit :class="'w-[60%] diy-dialog-wrap'" :title="formData.id ? t('updateAppVersion') : t('addAppVersion')">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
@@ -102,7 +102,7 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<view v-show="step == 1"> <view v-show="step == 1">
<el-button type="primary" class="ml-3" @click="step = 2">{{ <el-button type="primary" class="ml-3" @click="step = 2">{{
t('next') t('next')
@@ -116,7 +116,7 @@
</view> </view>
</span> </span>
</template> </template>
</el-dialog> </ModalAppVersionEdit>
<generate-sing-cert ref="generateSingCertRef"/> <generate-sing-cert ref="generateSingCertRef"/>
</template> </template>
@@ -127,8 +127,9 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { addVersion, editVersion, getVersionInfo, getAppPlatform } from '@/app/api/app' import { addVersion, editVersion, getVersionInfo, getAppPlatform } from '@/app/api/app'
import GenerateSingCert from '@/app/views/channel/app/components/generate-sing-cert.vue' import GenerateSingCert from '@/app/views/channel/app/components/generate-sing-cert.vue'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalAppVersionEdit, versionModalApi] = useVbenModal()
const loading = ref(false) const loading = ref(false)
const appPlatform = ref({}) const appPlatform = ref({})
const step = ref(1) const step = ref(1)
@@ -242,7 +243,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
save(formData).then(res => { save(formData).then(res => {
loading.value = false loading.value = false
showDialog.value = false versionModalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -263,7 +264,7 @@ const setFormData = async (row: any = null) => {
loading.value = false loading.value = false
} }
watch(() => showDialog.value, () => { watch(() => versionModalApi.getVisible?.(), () => {
step.value = 1 step.value = 1
}) })
@@ -271,10 +272,8 @@ const windowOpen = (url: string) => {
window.open(url) window.open(url)
} }
defineExpose({ const cancel = () => { versionModalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" title="生成Android证书" width="50%" class="diy-dialog-wrap" :destroy-on-close="true"> <ModalGenerateCert :class="'w-[50%] diy-dialog-wrap'" title="生成Android证书">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item label="证书别名" prop="key_alias"> <el-form-item label="证书别名" prop="key_alias">
@@ -62,11 +62,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button> <el-button @click="cancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">生成</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">生成</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalGenerateCert>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -74,8 +74,9 @@ import { ref, reactive, computed } from 'vue'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { generateSingCert } from '@/app/api/app' import { generateSingCert } from '@/app/api/app'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalGenerateCert, certModalApi] = useVbenModal()
const moreInfo = ref(false) const moreInfo = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -137,7 +138,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
generateSingCert(formData).then(res => { generateSingCert(formData).then(res => {
loading.value = false loading.value = false
showDialog.value = false certModalApi.close()
window.open(img(res.data), '_blank') window.open(img(res.data), '_blank')
}) })
} }
@@ -145,12 +146,11 @@ const confirm = async (formEl: FormInstance | undefined) => {
const open = async (row: any = null) => { const open = async (row: any = null) => {
Object.assign(formData, initialFormData) Object.assign(formData, initialFormData)
showDialog.value = true certModalApi.open()
} }
defineExpose({ const cancel = () => { certModalApi.close() }
open defineExpose({ open })
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -54,7 +54,7 @@
</div> </div>
</el-card> </el-card>
<el-dialog v-model="dialogVisible" :title="t('codeDownTwoDesc')" width="30%" :before-close="handleClose"> <ModalWeappRelease :class="'w-[30%]'" :title="t('codeDownTwoDesc')" @close="handleClose">
<el-form ref="ruleFormRef" :model="form" label-width="120px"> <el-form ref="ruleFormRef" :model="form" label-width="120px">
<el-form-item prop="code" :label="t('code')"> <el-form-item prop="code" :label="t('code')">
<el-input v-model.trim="form.code" :placeholder="t('codePlaceholder')" onkeyup="this.value = this.value.replace(/[^\d\.]/g,'');" /> <el-input v-model.trim="form.code" :placeholder="t('codePlaceholder')" onkeyup="this.value = this.value.replace(/[^\d\.]/g,'');" />
@@ -68,29 +68,29 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button> <el-button @click="releaseCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="insert"> <el-button type="primary" @click="insert">
{{ t('confirm') }} {{ t('confirm') }}
</el-button> </el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalWeappRelease>
<el-dialog v-model="failReasonDialogVisible" :title="t('failReason')" width="60%"> <ModalFailReason :class="'w-[60%]'" :title="t('failReason')">
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]"> <el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
<div v-html="failReason"></div> <div v-html="failReason"></div>
</el-scrollbar> </el-scrollbar>
</el-dialog> </ModalFailReason>
<el-dialog v-model="uploadSuccessShowDialog" :title="t('warning')" width="500px" draggable> <ModalUploadSuccess :class="'w-[500px]'" :title="t('warning')">
<span v-html="t('uploadSuccessTips')"></span> <span v-html="t('uploadSuccessTips')"></span>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
<el-button @click="knownToKnow" type="primary">{{ t('knownToKnow') }}</el-button> <el-button @click="knownToKnow" type="primary">{{ t('knownToKnow') }}</el-button>
<el-button @click="uploadSuccessShowDialog = false" type="primary" plain>{{ t('confirm') }}</el-button> <el-button @click="uploadSuccessCancel" type="primary" plain>{{ t('confirm') }}</el-button>
</div> </div>
</template> </template>
</el-dialog> </ModalUploadSuccess>
</div> </div>
</template> </template>
@@ -105,11 +105,13 @@ import { ElMessageBox } from 'element-plus'
import { AnyObject } from '@/types/global' import { AnyObject } from '@/types/global'
import Storage from '@/utils/storage' import Storage from '@/utils/storage'
import { siteWeappCommit, undoAudit } from "@/app/api/wxoplatform"; import { siteWeappCommit, undoAudit } from "@/app/api/wxoplatform";
import { useVbenModal } from '@vben/common-ui'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const pageName = route.meta.title const pageName = route.meta.title
const dialogVisible = ref(false) const dialogVisible = ref(false)
const [ModalWeappRelease, releaseModalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
const weappTableData:{ const weappTableData:{
page: number, page: number,
@@ -131,7 +133,9 @@ const form = ref({
content: '' content: ''
}) })
const uploadSuccessShowDialog = ref(false) const uploadSuccessShowDialog = ref(false)
const [ModalUploadSuccess, uploadSuccessModalApi] = useVbenModal()
const authCode = ref('') const authCode = ref('')
const [ModalFailReason, failReasonModalApi] = useVbenModal()
getAuthInfo().then(res => { getAuthInfo().then(res => {
if (res.data.data && res.data.data.auth_code) { if (res.data.data && res.data.data.auth_code) {
@@ -187,6 +191,7 @@ getWeappVersionListFn()
const handleClose = () => { const handleClose = () => {
ruleFormRef.value.clearValidate() ruleFormRef.value.clearValidate()
} }
const releaseCancel = () => { releaseModalApi.close() }
const uploading = ref(false) const uploading = ref(false)
const insert = () => { const insert = () => {
@@ -239,7 +244,7 @@ const getWeappUploadLogFn = (key: string) => {
if (last.code == 1 && last.percent == 100) { if (last.code == 1 && last.percent == 100) {
getWeappVersionListFn() getWeappVersionListFn()
getWeappPreviewImage() getWeappPreviewImage()
!Storage.get('weappUploadTipsLock') && (uploadSuccessShowDialog.value = true) !Storage.get('weappUploadTipsLock') && uploadSuccessModalApi.open()
return return
} }
setTimeout(() => { setTimeout(() => {
@@ -308,12 +313,15 @@ const failReasonDialogVisible = ref(false)
const handleFailReason = (data: any) => { const handleFailReason = (data: any) => {
failReason.value = data.fail_reason failReason.value = data.fail_reason
failReasonDialogVisible.value = true failReasonDialogVisible.value = true
failReasonModalApi.open()
} }
const knownToKnow = () => { const knownToKnow = () => {
Storage.set({ key: 'weappUploadTipsLock', data: true }) Storage.set({ key: 'weappUploadTipsLock', data: true })
uploadSuccessShowDialog.value = false uploadSuccessShowDialog.value = false
uploadSuccessModalApi.close()
} }
const uploadSuccessCancel = () => { uploadSuccessModalApi.close() }
const againUpload = () => { const againUpload = () => {
if (uploading.value) return if (uploading.value) return

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('functionSetting')" width="700px" :destroy-on-close="true"> <ModalWeappDomain :class="'w-[700px]'" :title="t('functionSetting')">
<el-form :model="formData" label-width="180px" ref="formRef" :rules="formRules" class="page-form pr-[100px]" v-loading="loading"> <el-form :model="formData" label-width="180px" ref="formRef" :rules="formRules" class="page-form pr-[100px]" v-loading="loading">
<el-form-item :label="t('requestUrl')" prop="requestdomain"> <el-form-item :label="t('requestUrl')" prop="requestdomain">
<el-input v-model="formData.requestdomain" :placeholder="t('requestdomainPlaceholder')" type="textarea"> <el-input v-model="formData.requestdomain" :placeholder="t('requestdomainPlaceholder')" type="textarea">
@@ -29,11 +29,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalWeappDomain>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -42,8 +42,9 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { setWeappDomain } from '@/app/api/weapp' import { setWeappDomain } from '@/app/api/weapp'
import Test from '@/utils/test' import Test from '@/utils/test'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalWeappDomain, domainModalApi] = useVbenModal()
const loading = ref(false) const loading = ref(false)
/** /**
@@ -135,7 +136,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
setWeappDomain(data).then(res => { setWeappDomain(data).then(res => {
loading.value = false loading.value = false
showDialog.value = false domainModalApi.close()
emit('complete', data) emit('complete', data)
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -154,10 +155,8 @@ const setFormData = async (data: any = null) => {
} }
} }
defineExpose({ const cancel = () => { domainModalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('privacyAgreementTitle')" width="900px" :destroy-on-close="true"> <ModalWeappPrivacy :class="'w-[900px]'" :title="t('privacyAgreementTitle')">
<div class="h-[60vh]"> <div class="h-[60vh]">
<el-scrollbar> <el-scrollbar>
<el-form :model="formData" label-width="auto" label-position="left" ref="formRef" :rules="formRules" <el-form :model="formData" label-width="auto" label-position="left" ref="formRef" :rules="formRules"
@@ -95,11 +95,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalWeappPrivacy>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -108,8 +108,9 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import SettingList from './setting-list.vue' import SettingList from './setting-list.vue'
import { getWeappPrivacySetting, setWeappPrivacySetting } from '@/app/api/weapp' import { getWeappPrivacySetting, setWeappPrivacySetting } from '@/app/api/weapp'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalWeappPrivacy, privacyModalApi] = useVbenModal()
const loading = ref(false) const loading = ref(false)
const settingListRef = ref(null) const settingListRef = ref(null)
const sdkSettingListRef = ref(null) const sdkSettingListRef = ref(null)
@@ -187,7 +188,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
setWeappPrivacySetting(data).then(res => { setWeappPrivacySetting(data).then(res => {
loading.value = false loading.value = false
showDialog.value = false privacyModalApi.close()
emit('complete', data) emit('complete', data)
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -209,14 +210,12 @@ const setFormData = async () => {
formData.store_expire_timestamp = data.owner_setting.store_expire_timestamp formData.store_expire_timestamp = data.owner_setting.store_expire_timestamp
} }
} }
showDialog.value = true privacyModalApi.open()
}) })
} }
defineExpose({ const cancel = () => { privacyModalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -114,7 +114,7 @@
</div> </div>
</div> </div>
<el-dialog v-model="settingTypeDialog" :title="t('settingTypeTitle')" width="500px" :destroy-on-close="true"> <ModalSettingType :class="'w-[500px]'" :title="t('settingTypeTitle')">
<el-checkbox-group v-model="checkList"> <el-checkbox-group v-model="checkList">
<template v-for="(item, index) in privacyList"> <template v-for="(item, index) in privacyList">
<el-checkbox :label="item.privacy_key" v-if="!checkIsSelected(item.privacy_key)">{{ item.privacy_text }}</el-checkbox> <el-checkbox :label="item.privacy_key" v-if="!checkIsSelected(item.privacy_key)">{{ item.privacy_text }}</el-checkbox>
@@ -122,16 +122,17 @@
</el-checkbox-group> </el-checkbox-group>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="settingTypeDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="selectSettingType()">{{ t('confirm') }}</el-button> <el-button type="primary" @click="selectSettingType()">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalSettingType>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@@ -142,6 +143,7 @@ const props = defineProps({
} }
}) })
const [ModalSettingType, settingModalApi] = useVbenModal()
const settingTypeDialog = ref(false) const settingTypeDialog = ref(false)
const privacyList = ref([ const privacyList = ref([
@@ -199,11 +201,13 @@ const selectSettingType = () => {
}) })
}) })
settingTypeDialog.value = false settingTypeDialog.value = false
settingModalApi.close()
checkList.value = [] checkList.value = []
} }
const addSettingList = () => { const addSettingList = () => {
settingTypeDialog.value = true settingTypeDialog.value = true
settingModalApi.open()
} }
const selectedSettingType = computed(() => { const selectedSettingType = computed(() => {
@@ -219,3 +223,4 @@ defineExpose({
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
const cancel = () => { settingTypeDialog.value = false; settingModalApi.close() }

View File

@@ -2,7 +2,7 @@
<div @click="openDialog"> <div @click="openDialog">
<slot></slot> <slot></slot>
</div> </div>
<el-dialog v-model="showDialog" :title="t('upload.select' + type)" width="60%" class="attachment-dialog" :destroy-on-close="true"> <ModalWechatMedia :class="'w-[60%] attachment-dialog'" :title="t('upload.select' + type)">
<div class="flex border-t border-b main-wrap border-color w-full h-[40vh]"> <div class="flex border-t border-b main-wrap border-color w-full h-[40vh]">
<!-- 素材 --> <!-- 素材 -->
<div class="attachment-list-wrap flex flex-col p-[15px] flex-1 overflow-hidden"> <div class="attachment-list-wrap flex flex-col p-[15px] flex-1 overflow-hidden">
@@ -88,11 +88,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="confirm">{{ t('confirm') }}</el-button> <el-button type="primary" @click="confirm">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalWechatMedia>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -101,6 +101,7 @@ import { t } from '@/lang'
import UploadMedia from './upload-media.vue' import UploadMedia from './upload-media.vue'
import { img, debounce } from '@/utils/common' import { img, debounce } from '@/utils/common'
import { getMediaList, syncNews } from '@/app/api/wechat' import { getMediaList, syncNews } from '@/app/api/wechat'
import { useVbenModal } from '@vben/common-ui'
const prop = defineProps({ const prop = defineProps({
type: { type: {
@@ -109,11 +110,11 @@ const prop = defineProps({
} }
}) })
const showDialog = ref(false) const [ModalWechatMedia, mediaModalApi] = useVbenModal()
const openDialog = () => { const openDialog = () => {
prop.type == 'news' && waterfall() prop.type == 'news' && waterfall()
showDialog.value = true mediaModalApi.open()
} }
const attachment: Record<string, any> = reactive({ const attachment: Record<string, any> = reactive({
@@ -150,7 +151,9 @@ const selectedFile: Record<string, any> = ref({})
const confirm = () => { const confirm = () => {
emits('success', selectedFile.value) emits('success', selectedFile.value)
mediaModalApi.close()
} }
const cancel = () => { mediaModalApi.close() }
const syncLoading = ref(false) const syncLoading = ref(false)
const syncWechatNews = () => { const syncWechatNews = () => {

View File

@@ -66,16 +66,16 @@
</div> </div>
</div> </div>
<el-dialog v-model="showDialog" :title="t('addReplyContent')" width="60%" :destroy-on-close="true"> <ModalReplyContent :class="'w-[60%]'" :title="t('addReplyContent')">
<reply-form v-model="replyContent" ref="ReplyRef"/> <reply-form v-model="replyContent" ref="ReplyRef"/>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancelReply">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="addReplyContent">{{ t('confirm') }}</el-button> <el-button type="primary" @click="addReplyContent">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalReplyContent>
</div> </div>
</template> </template>
@@ -88,6 +88,7 @@ import { ArrowLeft } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import ReplyForm from '@/app/views/channel/wechat/components/reply-form.vue' import ReplyForm from '@/app/views/channel/wechat/components/reply-form.vue'
import NewsCard from '@/app/views/channel/wechat/components/news-card.vue' import NewsCard from '@/app/views/channel/wechat/components/news-card.vue'
import { useVbenModal } from '@vben/common-ui'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -96,6 +97,7 @@ const back = () => {
router.push('/channel/wechat/reply') router.push('/channel/wechat/reply')
} }
const [ModalReplyContent, replyModalApi] = useVbenModal()
const showDialog = ref(false) const showDialog = ref(false)
const formData: any = reactive({ const formData: any = reactive({
@@ -116,9 +118,11 @@ const addReplyContent = () => {
formData.content.push(replyContent.value) formData.content.push(replyContent.value)
replyContent.value = {} replyContent.value = {}
showDialog.value = false showDialog.value = false
replyModalApi.close()
} }
}) })
} }
const cancelReply = () => { showDialog.value = false; replyModalApi.close() }
const removeContent = (index: number) => { const removeContent = (index: number) => {
formData.content.splice(index, 1) formData.content.splice(index, 1)

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('dictData')" width="60%" class="diy-dialog-wrap" :destroy-on-close="true"> <ModalDict :class="'w-[60%] diy-dialog-wrap'" :title="t('dictData')">
<div class="mb-[10px]"> <div class="mb-[10px]">
<el-button type="primary" @click="addEvent"> <el-button type="primary" @click="addEvent">
{{ t('addDictData') }} {{ t('addDictData') }}
@@ -20,11 +20,11 @@
</el-table> </el-table>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="confirm()">{{ t('confirm') }}</el-button> <el-button type="primary" @click="confirm()">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
<el-dialog v-model="dialogVisible" :title="type != 'edit' ? t('addDictData') : t('editDictData')" width="480" class="diy-dialog-wrap" :destroy-on-close="true"> <ModalDictItem :class="'w-[480px] diy-dialog-wrap'" :title="type != 'edit' ? t('addDictData') : t('editDictData')">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form"> <el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form">
<el-form-item :label="t('name')"> <el-form-item :label="t('name')">
<el-input v-model.trim="name" disabled class="input-width" /> <el-input v-model.trim="name" disabled class="input-width" />
@@ -47,12 +47,12 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button> <el-button @click="itemCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="submit(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" @click="submit(formRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalDictItem>
</el-dialog> </ModalDict>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -61,10 +61,11 @@ import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { setDictData, getDictInfo } from '@/app/api/dict' import { setDictData, getDictInfo } from '@/app/api/dict'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalDict, dictModalApi] = useVbenModal()
const loading = ref(false) const loading = ref(false)
const dialogVisible = ref(false) const [ModalDictItem, itemModalApi] = useVbenModal()
const tableDate = ref<Array<any>>([]) const tableDate = ref<Array<any>>([])
const id = ref() const id = ref()
@@ -95,7 +96,7 @@ const formRules = computed(() => {
const addEvent = () => { const addEvent = () => {
type.value = 'add' type.value = 'add'
formData.value = cloneDeep(initialFormData) formData.value = cloneDeep(initialFormData)
dialogVisible.value = true itemModalApi.open()
} }
const tableIndex = ref(0) const tableIndex = ref(0)
const editEvent = (row: any, index: number) => { const editEvent = (row: any, index: number) => {
@@ -103,7 +104,7 @@ const editEvent = (row: any, index: number) => {
tableIndex.value = index tableIndex.value = index
formData.value = cloneDeep(initialFormData) formData.value = cloneDeep(initialFormData)
formData.value = Object.assign(formData.value, cloneDeep(row)) formData.value = Object.assign(formData.value, cloneDeep(row))
dialogVisible.value = true itemModalApi.open()
} }
/** /**
* 表单确认 * 表单确认
@@ -118,7 +119,7 @@ const submit = async (formEl: FormInstance | undefined) => {
tableDate.value.splice(tableIndex.value, 1, cloneDeep(formData.value)) tableDate.value.splice(tableIndex.value, 1, cloneDeep(formData.value))
} }
tableDate.value.sort(function (a, b) { return b.sort - a.sort }) tableDate.value.sort(function (a, b) { return b.sort - a.sort })
dialogVisible.value = false itemModalApi.close()
} }
}) })
} }
@@ -137,7 +138,7 @@ const confirm = async () => {
loading.value = true loading.value = true
setDictData(id.value, { dictionary: JSON.stringify(tableDate.value) }).then(res => { setDictData(id.value, { dictionary: JSON.stringify(tableDate.value) }).then(res => {
loading.value = false loading.value = false
showDialog.value = false dictModalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -145,7 +146,7 @@ const confirm = async () => {
} }
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
showDialog.value = true dictModalApi.open()
loading.value = true loading.value = true
id.value = row.id id.value = row.id
name.value = row.name name.value = row.name
@@ -154,10 +155,9 @@ const setFormData = async (row: any = null) => {
loading.value = false loading.value = false
} }
defineExpose({ const cancel = () => { dictModalApi.close() }
showDialog, const itemCancel = () => { itemModalApi.close() }
setFormData defineExpose({ setFormData })
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,34 +1,17 @@
<template> <template>
<el-dialog v-model="showDialog" :title="formData.id ? t('updateDict') : t('addDict')" width="480" class="diy-dialog-wrap" :destroy-on-close="true"> <Modal :class="'w-[480px]'" :title="formModel.id ? t('updateDict') : t('addDict')">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('name')" prop="name"> <p class="form-tip">{{ t('keyFormatTips') }}</p>
<el-input v-model.trim="formData.name" clearable maxlength="40" show-word-limit :placeholder="t('namePlaceholder')" class="input-width" /> </Modal>
</el-form-item>
<el-form-item :label="t('key')" prop="key">
<el-input v-model.trim="formData.key" clearable maxlength="40" show-word-limit :placeholder="t('keyPlaceholder')" class="input-width" />
<p class="form-tip">{{ t('keyFormatTips') }}</p>
</el-form-item>
<el-form-item :label="t('memo')">
<el-input v-model.trim="formData.memo" type="textarea" clearable :placeholder="t('memoPlaceholder')" class="input-width" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { addDict, editDict, getDictInfo } from '@/app/api/dict' import { addDict, editDict, getDictInfo } from '@/app/api/dict'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false)
const loading = ref(false) const loading = ref(false)
/** /**
@@ -40,80 +23,41 @@ const initialFormData = {
key: '', key: '',
memo: '' memo: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const [Modal, modalApi] = useVbenModal()
const formRef = ref<FormInstance>() const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
// 表单验证规则 handleSubmit: async (values) => {
const formRules = computed(() => { loading.value = true
return { const save = values.id ? editDict : addDict
name: [ save(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
{ required: true, message: t('namePlaceholder'), trigger: 'blur' } },
], layout: 'horizontal',
key: [ schema: [
{ required: true, message: t('keyPlaceholder'), trigger: 'blur' }, { component: 'Input', fieldName: 'name', label: t('name'), rules: [{ required: true }] },
{ { component: 'Input', fieldName: 'key', label: t('key'), rules: [ { required: true }, { validator: (v) => (/^[a-zA-Z_]+$/.test(v) ? true : t('keyFormatTips')) } ] },
validator: (rule: any, value: any, callback: any) => { { component: 'Textarea', fieldName: 'memo', label: t('memo') }
if (/^[a-zA-Z_]+$/.test(value)) { ],
callback() wrapperClass: 'grid-cols-1'
} else {
callback(new Error(t('keyFormatTips')))
}
},
trigger: 'blur'
}
],
data: [
{ required: true, message: t('dataPlaceholder'), trigger: 'blur' }
]
}
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
/** const confirm = async () => { if (loading.value) return; formApi.submit() }
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
const save = formData.id ? editDict : addDict
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
save(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
loading.value = true loading.value = true
if (row) { if (row) {
const data = await (await getDictInfo(row.id)).data const data = await (await getDictInfo(row.id)).data
if (data) { if (data) { Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] }) }
Object.keys(formData).forEach((key: string) => { }
if (data[key] != undefined) formData[key] = data[key] formApi.setModel({ ...formModel })
}) loading.value = false
} modalApi.open()
}
loading.value = false
} }
defineExpose({ defineExpose({ setFormData })
showDialog,
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -89,7 +89,7 @@
</el-form> </el-form>
</div> </div>
<el-dialog v-model="showDialog" :title="t('selectStyle')" width="800px"> <ModalStyleSelect :class="'w-[800px]'" :title="t('selectStyle')">
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="flex items-center justify-center overflow-hidden w-[32%] h-[100px] mr-[2%] mb-[15px] cursor-pointer border bg-gray-50" <div class="flex items-center justify-center overflow-hidden w-[32%] h-[100px] mr-[2%] mb-[15px] cursor-pointer border bg-gray-50"
@@ -112,12 +112,12 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="changeStyle">{{ t('confirm') }}</el-button> <el-button type="primary" @click="changeStyle">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalStyleSelect>
</div> </div>
<!-- 样式 --> <!-- 样式 -->
@@ -178,6 +178,7 @@ import { t } from '@/lang'
import { watch, ref } from 'vue' import { watch, ref } from 'vue'
import useDiyStore from '@/stores/modules/diy' import useDiyStore from '@/stores/modules/diy'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const diyStore = useDiyStore() const diyStore = useDiyStore()
@@ -212,9 +213,9 @@ watch(
}, { deep: true } }, { deep: true }
) )
const showDialog = ref(false) const [ModalStyleSelect, styleModalApi] = useVbenModal()
const showStyle = () => { const showStyle = () => {
showDialog.value = true styleModalApi.open()
} }
const selectStyle = ref('style-1') const selectStyle = ref('style-1')
@@ -234,8 +235,9 @@ const changeStyle = () => {
break break
} }
diyStore.global.topStatusBar.style = selectStyle.value diyStore.global.topStatusBar.style = selectStyle.value
showDialog.value = false styleModalApi.close()
} }
const cancel = () => { styleModalApi.close() }
const selectImg = (url: any) => { const selectImg = (url: any) => {
const image = new Image() const image = new Image()

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="dialogThemeVisible" title="编辑色调" width="850px" align-center destroy-on-close="true"> <ModalEditTheme :class="'w-[850px]'" title="编辑色调" align-center>
<el-form :model="openData" label-width="150px" :rules="formRules"> <el-form :model="openData" label-width="150px" :rules="formRules">
<el-form-item label="色调名称" prop="title"> <el-form-item label="色调名称" prop="title">
<el-input v-model="openData.title" placeholder="请输入色调名称" maxlength="15" class="!w-[250px]" :disabled="openData.id != ''" @keydown.enter.native.prevent /> <el-input v-model="openData.title" placeholder="请输入色调名称" maxlength="15" class="!w-[250px]" :disabled="openData.id != ''" @keydown.enter.native.prevent />
@@ -34,12 +34,12 @@
<add-theme-component ref="addThemeRef" @confirm="addThemeConfirm" /> <add-theme-component ref="addThemeRef" @confirm="addThemeConfirm" />
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogThemeVisible = false">取消</el-button> <el-button @click="cancel">取消</el-button>
<el-button type="primary" plain @click="resetConfirmFn()">重置</el-button> <el-button type="primary" plain @click="resetConfirmFn()">重置</el-button>
<el-button type="primary" @click="confirmFn(formRef)">保存</el-button> <el-button type="primary" @click="confirmFn(formRef)">保存</el-button>
</div> </div>
</template> </template>
</el-dialog> </ModalEditTheme>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -51,10 +51,11 @@ import addThemeComponent from './add-theme.vue'
import useDiyStore from '@/stores/modules/diy' import useDiyStore from '@/stores/modules/diy'
import { addTheme, editTheme } from '@/app/api/diy' import { addTheme, editTheme } from '@/app/api/diy'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { useVbenModal } from '@vben/common-ui'
const diyStore = useDiyStore() const diyStore = useDiyStore()
const dialogThemeVisible = ref(false) const [ModalEditTheme, themeModalApi] = useVbenModal()
const addThemeRef = ref() const addThemeRef = ref()
const openData: Record<string, any> = reactive({ // 用于接收弹窗打开时的参数 const openData: Record<string, any> = reactive({ // 用于接收弹窗打开时的参数
title: '', title: '',
@@ -93,7 +94,7 @@ const open = (res: any) => { // 参数: title=>色调名称key=>区分系
formData.value.forEach((item, index) => { formData.value.forEach((item, index) => {
item.value = res.theme[item.label] ? res.theme[item.label] : item.value item.value = res.theme[item.label] ? res.theme[item.label] : item.value
}) })
dialogThemeVisible.value = true themeModalApi.open()
} }
// 新增颜色 // 新增颜色
@@ -218,7 +219,7 @@ const confirmFn = async (formEl: FormInstance | undefined) => {
api(params).then((res: any) => { api(params).then((res: any) => {
confirmRepeat = false confirmRepeat = false
dialogThemeVisible.value = false themeModalApi.close()
emit('confirm', params) emit('confirm', params)
}).catch(() => { }).catch(() => {
confirmRepeat = false confirmRepeat = false
@@ -226,6 +227,7 @@ const confirmFn = async (formEl: FormInstance | undefined) => {
} }
}) })
} }
const cancel = () => { themeModalApi.close() }
const applyOpacity = (color, opacity) => { const applyOpacity = (color, opacity) => {
// 解析十六进制或 RGBA 格式 // 解析十六进制或 RGBA 格式
@@ -258,10 +260,7 @@ const colorPickerChange = (e: any, data: any) => {
}) })
} }
} }
defineExpose({ defineExpose({ open })
dialogThemeVisible,
open
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<el-dialog v-model="showDialog" :title="t('submitSuccess')" width="850px" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false"> <ModalFormSubmit :class="'w-[850px]'" :title="t('submitSuccess')" :close-on-click-modal="false">
<div class="flex flex-1 mt-[24px] mx-[24px] mb-0"> <div class="flex flex-1 mt-[24px] mx-[24px] mb-0">
<div class="preview-wrap"> <div class="preview-wrap">
@@ -209,7 +209,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="confirm">{{ t('save') }}</el-button> <el-button type="primary" @click="confirm">{{ t('save') }}</el-button>
</div> </div>
</template> </template>
@@ -227,7 +227,9 @@ import storage from '@/utils/storage'
import { filterNumber } from '@/utils/common' import { filterNumber } from '@/utils/common'
import { getUrl } from '@/app/api/sys' import { getUrl } from '@/app/api/sys'
import { getFormSubmitConfig,editDiyFormSubmitConfig } from '@/app/api/diy_form' import { getFormSubmitConfig,editDiyFormSubmitConfig } from '@/app/api/diy_form'
import { useVbenModal } from '@vben/common-ui'
const [ModalFormSubmit, formSubmitModalApi] = useVbenModal()
const showDialog = ref(false) const showDialog = ref(false)
const repeat = ref(false) const repeat = ref(false)
@@ -350,12 +352,15 @@ const confirm = () => {
editDiyFormSubmitConfig(data).then(res => { editDiyFormSubmitConfig(data).then(res => {
repeat.value = false repeat.value = false
showDialog.value = false showDialog.value = false
formSubmitModalApi.close()
emit('complete') emit('complete')
}).catch(err => { }).catch(err => {
repeat.value = false repeat.value = false
}) })
} }
const cancel = () => { showDialog.value = false; formSubmitModalApi.close() }
defineExpose({ defineExpose({
showDialog, showDialog,
setFormData setFormData

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('writeSet')" width="600px" class="diy-dialog-wrap" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false"> <ModalFormWrite :class="'w-[600px] diy-dialog-wrap'" :title="t('writeSet')" :close-on-click-modal="false">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
@@ -108,11 +108,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalFormWrite>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -122,7 +122,9 @@ import type { FormInstance } from 'element-plus'
import { filterNumber } from '@/utils/common' import { filterNumber } from '@/utils/common'
import {getMemberLabelAll,getMemberLevelAll } from '@/app/api/member' import {getMemberLabelAll,getMemberLevelAll } from '@/app/api/member'
import { getFormWriteConfig,editDiyFormWriteConfig } from '@/app/api/diy_form' import { getFormWriteConfig,editDiyFormWriteConfig } from '@/app/api/diy_form'
import { useVbenModal } from '@vben/common-ui'
const [ModalFormWrite, formWriteModalApi] = useVbenModal()
const showDialog = ref(false) const showDialog = ref(false)
const loading = ref(false) const loading = ref(false)
@@ -308,6 +310,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
editDiyFormWriteConfig(data).then(res => { editDiyFormWriteConfig(data).then(res => {
loading.value = false loading.value = false
showDialog.value = false showDialog.value = false
formWriteModalApi.close()
emit('complete') emit('complete')
}).catch(err => { }).catch(err => {
loading.value = false loading.value = false
@@ -325,6 +328,7 @@ defineExpose({
showDialog, showDialog,
setFormData setFormData
}) })
const cancel = () => { showDialog.value = false; formWriteModalApi.close() }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -94,7 +94,7 @@
</el-card> </el-card>
<!--添加表单--> <!--添加表单-->
<el-dialog v-model="dialogVisible" :title="t('addFormTips')" width="980px"> <ModalAddFormTips :class="'w-[980px]'" :title="t('addFormTips')">
<el-form :model="formData" ref="formRef" :rules="formRules"> <el-form :model="formData" ref="formRef" :rules="formRules">
<!-- <el-form-item :label="t('title')" prop="title">--> <!-- <el-form-item :label="t('title')" prop="title">-->
<!-- <el-input v-model.trim="formData.title" :placeholder="t('titlePlaceholder')" clearable maxlength="12" show-word-limit class="w-full" />--> <!-- <el-input v-model.trim="formData.title" :placeholder="t('titlePlaceholder')" clearable maxlength="12" show-word-limit class="w-full" />-->
@@ -120,14 +120,14 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button> <el-button @click="cancelAddForm">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="addEvent(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" @click="addEvent(formRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalAddFormTips>
<!-- 分享设置--> <!-- 分享设置-->
<el-dialog v-model="shareDialogVisible" :title="t('shareSet')" width="30%"> <ModalShareSet :class="'w-[30%]'" :title="t('shareSet')">
<el-tabs v-model="tabShareType"> <el-tabs v-model="tabShareType">
<el-tab-pane :label="t('wechat')" name="wechat"></el-tab-pane> <el-tab-pane :label="t('wechat')" name="wechat"></el-tab-pane>
<el-tab-pane :label="t('weapp')" name="weapp"></el-tab-pane> <el-tab-pane :label="t('weapp')" name="weapp"></el-tab-pane>
@@ -149,11 +149,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="shareDialogVisible = false">{{ t('cancel') }}</el-button> <el-button @click="cancelShare">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="shareEvent(shareFormRef)">{{ t('confirm') }}</el-button> <el-button type="primary" @click="shareEvent(shareFormRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalShareSet>
<!-- 推广弹出框 --> <!-- 推广弹出框 -->
<spread-popup ref="spreadPopupRef" /> <spread-popup ref="spreadPopupRef" />
@@ -179,6 +179,7 @@ import { getFormType, getApps, getDiyFormPageList, deleteDiyForm, editDiyFormSha
import { FormInstance, ElMessage, ElMessageBox } from 'element-plus' import { FormInstance, ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { setTablePageStorage, getTablePageStorage, img } from '@/utils/common' import { setTablePageStorage, getTablePageStorage, img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
import recordsDetail from '@/app/views/diy_form/records.vue' import recordsDetail from '@/app/views/diy_form/records.vue'
import formSubmitPopup from '@/app/views/diy_form/components/form-submit-popup.vue' import formSubmitPopup from '@/app/views/diy_form/components/form-submit-popup.vue'
@@ -219,6 +220,7 @@ const formRules = computed(() => {
}) })
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const [ModalAddFormTips, addFormModalApi] = useVbenModal()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const addEvent = async (formEl: FormInstance | undefined) => { const addEvent = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
@@ -232,11 +234,13 @@ const addEvent = async (formEl: FormInstance | undefined) => {
}) })
window.open(url.href) window.open(url.href)
dialogVisible.value = false dialogVisible.value = false
addFormModalApi.close()
formData.title = '' formData.title = ''
formData.type = '' formData.type = ''
} }
}) })
} }
const cancelAddForm = () => { dialogVisible.value = false; addFormModalApi.close() }
const showClick = (row: any) => { const showClick = (row: any) => {
const data = row.status === 1 ? 0 : 1 const data = row.status === 1 ? 0 : 1
@@ -472,6 +476,7 @@ const shareFormData = reactive({
} }
}) })
const [ModalShareSet, shareModalApi] = useVbenModal()
const shareDialogVisible = ref(false) const shareDialogVisible = ref(false)
const shareFormRules = computed(() => { const shareFormRules = computed(() => {
return {} return {}
@@ -486,6 +491,7 @@ const openShare = async (row: any) => {
shareFormData.weapp = share.weapp shareFormData.weapp = share.weapp
shareDialogVisible.value = true shareDialogVisible.value = true
shareModalApi.open()
} }
const shareEvent = async (formEl: FormInstance | undefined) => { const shareEvent = async (formEl: FormInstance | undefined) => {
@@ -499,11 +505,13 @@ const shareEvent = async (formEl: FormInstance | undefined) => {
}).then(() => { }).then(() => {
loadDiyFormList(getTablePageStorage(diyFormTableData.searchParam).page) loadDiyFormList(getTablePageStorage(diyFormTableData.searchParam).page)
shareDialogVisible.value = false shareDialogVisible.value = false
shareModalApi.close()
}).catch(() => { }).catch(() => {
}) })
} }
}) })
} }
const cancelShare = () => { shareDialogVisible.value = false; shareModalApi.close() }
// 表单推广 // 表单推广
const spreadPopupRef = ref(null) const spreadPopupRef = ref(null)

View File

@@ -150,7 +150,7 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<el-dialog v-model="dialogVisible" :title="t('viewInformation')" width="400px"> <ModalDiyFormView :class="'w-[400px]'" :title="t('viewInformation')">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex mb-[10px]" v-for="(item, index) in formDetail" :key="index"> <div class="flex mb-[10px]" v-for="(item, index) in formDetail" :key="index">
<div class="flex justify-end w-[100px]">{{ item.label }}</div> <div class="flex justify-end w-[100px]">{{ item.label }}</div>
@@ -167,10 +167,10 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="dialogVisible = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="closeView">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalDiyFormView>
</el-drawer> </el-drawer>
<export-sure ref="exportSureDialog" :show="flag" type="diy_form_records" :searchParam="formData.searchParam" @close="handleExportClose" /> <export-sure ref="exportSureDialog" :show="flag" type="diy_form_records" :searchParam="formData.searchParam" @close="handleExportClose" />
@@ -190,6 +190,7 @@ const router = useRouter()
const showDialog = ref(false) const showDialog = ref(false)
const activeName = ref('detail_data') const activeName = ref('detail_data')
const formId = ref(0) const formId = ref(0)
const [ModalDiyFormView, viewModalApi] = useVbenModal()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const searchFormDiyFormRef = ref<FormInstance>() const searchFormDiyFormRef = ref<FormInstance>()
const searchFormDiyMemberRef = ref<FormInstance>() const searchFormDiyMemberRef = ref<FormInstance>()
@@ -241,8 +242,10 @@ const formDetailEvent = (row: any) => {
getFormRecordsInfo(row.record_id).then((res:any) => { getFormRecordsInfo(row.record_id).then((res:any) => {
formDetail.value = res.data.value formDetail.value = res.data.value
dialogVisible.value = true dialogVisible.value = true
viewModalApi.open()
}) })
} }
const closeView = () => { dialogVisible.value = false; viewModalApi.close() }
// 删除 // 删除
const deleteEvent = (row: any) => { const deleteEvent = (row: any) => {
@@ -388,3 +391,4 @@ defineExpose({
} }
} }
</style> </style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -95,7 +95,7 @@
</div> </div>
</el-card> </el-card>
<el-dialog v-model="showDialog" :title="t('accountDetail')" width="550px" :destroy-on-close="true"> <ModalAccountDetail :class="'w-[550px]'" :title="t('accountDetail')">
<el-form :model="formData" label-width="110px" ref="formRef" class="page-form"> <el-form :model="formData" label-width="110px" ref="formRef" class="page-form">
<!-- <el-form-item :label="t('tradeNo')">--> <!-- <el-form-item :label="t('tradeNo')">-->
@@ -159,10 +159,10 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalAccountDetail>
</div> </div>
</template> </template>
@@ -232,6 +232,7 @@ const checkAccountType = () => {
}) })
} }
checkAccountType() checkAccountType()
const [ModalAccountDetail, accountDetailModalApi] = useVbenModal()
const showDialog = ref(false) const showDialog = ref(false)
const formData = ref({ const formData = ref({
trade_no: '', trade_no: '',
@@ -253,8 +254,10 @@ const formData = ref({
}) })
const detailEvent = (info:any) => { const detailEvent = (info:any) => {
showDialog.value = true showDialog.value = true
accountDetailModalApi.open()
formData.value = info formData.value = info
} }
const cancel = () => { showDialog.value = false; accountDetailModalApi.close() }
interface AccountStat{ interface AccountStat{
pay: number, pay: number,
@@ -274,3 +277,4 @@ checkAccountStat()
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -207,7 +207,7 @@
</el-card> </el-card>
<!-- 详情 --> <!-- 详情 -->
<el-dialog v-model="cashOutShowDialog" :title="t('cashOutDetail')" width="650px" :destroy-on-close="true"> <ModalDetail :class="'w-[650px]'" :title="t('cashOutDetail')">
<el-form :model="cashOutInfo" label-width="120px" ref="formRef" class="page-form" v-loading="cashOutLoading"> <el-form :model="cashOutInfo" label-width="120px" ref="formRef" class="page-form" v-loading="cashOutLoading">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
@@ -309,12 +309,12 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="cashOutShowDialog = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="detailClose">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalDetail>
<!-- 审核通过 --> <!-- 审核通过 -->
<el-dialog v-model="auditPassShowDialog" :title="t('passAudit')" width="650px" :destroy-on-close="true"> <ModalAuditPass :class="'w-[650px]'" :title="t('passAudit')">
<el-form :model="curData" label-width="120px" ref="formRef" class="page-form"> <el-form :model="curData" label-width="120px" ref="formRef" class="page-form">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
@@ -390,13 +390,13 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="auditPassShowDialog = false">{{ t('cancel') }}</el-button> <el-button @click="auditPassCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="handlePass()">{{ t('confirm') }}</el-button> <el-button type="primary" @click="handlePass()">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalAuditPass>
<!-- 是否审核拒绝 --> <!-- 是否审核拒绝 -->
<el-dialog v-model="auditShowDialog" :title="t('rejectionAudit')" width="500px" :destroy-on-close="true"> <ModalAuditRefuse :class="'w-[500px]'" :title="t('rejectionAudit')">
<el-form :model="auditFailure" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="auditFailure" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('reasonsRefusal')" prop="refuse_reason"> <el-form-item :label="t('reasonsRefusal')" prop="refuse_reason">
<el-input v-model.trim="auditFailure.refuse_reason" clearable maxlength="200" :show-word-limit="true" :placeholder="t('reasonsRefusalPlaceholder')" :rows="4" class="input-width" type="textarea" /> <el-input v-model.trim="auditFailure.refuse_reason" clearable maxlength="200" :show-word-limit="true" :placeholder="t('reasonsRefusalPlaceholder')" :rows="4" class="input-width" type="textarea" />
@@ -404,14 +404,14 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="auditShowDialog = false">{{ t('cancel') }}</el-button> <el-button @click="auditRefuseCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalAuditRefuse>
<!-- 是否转账 --> <!-- 是否转账 -->
<el-dialog v-model="transferShowDialog" :title="t('transfer')" width="650px" :destroy-on-close="true"> <ModalTransfer :class="'w-[650px]'" :title="t('transfer')">
<el-form :model="transferData" label-width="120px" ref="formRef" class="page-form"> <el-form :model="transferData" label-width="120px" ref="formRef" class="page-form">
<el-row> <el-row>
<template v-if="transferData.transfer_type == 'alipay' || transferData.transfer_type == 'wechat_code'"> <template v-if="transferData.transfer_type == 'alipay' || transferData.transfer_type == 'wechat_code'">
@@ -476,13 +476,13 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="transferShowDialog = false">{{ t('cancel') }}</el-button> <el-button @click="transferCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="handleTransfer(formTransferRef)">{{ t('confirm') }}</el-button> <el-button type="primary" @click="handleTransfer(formTransferRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalTransfer>
<!-- 备注 --> <!-- 备注 -->
<el-dialog v-model="remarkShowDialog" :title="t('remark')" width="500px" :destroy-on-close="true"> <ModalRemark :class="'w-[500px]'" :title="t('remark')">
<el-form :model="formData" label-width="90px" ref="formRemarkRef" :rules="formRemarkRules" class="page-form"> <el-form :model="formData" label-width="90px" ref="formRemarkRef" :rules="formRemarkRules" class="page-form">
<el-form-item :label="t('remark')" prop="remark"> <el-form-item :label="t('remark')" prop="remark">
<el-input v-model.trim="formData.remark" type="textarea" rows="4" clearable <el-input v-model.trim="formData.remark" type="textarea" rows="4" clearable
@@ -491,11 +491,11 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="remarkShowDialog = false">{{ t('cancel') }}</el-button> <el-button @click="remarkCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="save(formRemarkRef)">{{ t('confirm') }}</el-button> <el-button type="primary" @click="save(formRemarkRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalRemark>
</div> </div>
</template> </template>
@@ -506,6 +506,7 @@ import { getCashOutList, getTransfertype, memberTransfer, memberAudit, getCashOu
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { ElMessageBox, FormInstance, FormRules } from 'element-plus' import { ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useVbenModal } from '@vben/common-ui'
const cashOutStatusList = ref([]) const cashOutStatusList = ref([])
const checkStatusList = async () => { const checkStatusList = async () => {
@@ -608,28 +609,29 @@ const formTransferRules = computed(() => {
const transferFn = (data:any) => { const transferFn = (data:any) => {
transferData.value = data transferData.value = data
formTransfer.id = data.id formTransfer.id = data.id
transferShowDialog.value = true modalTransferApi.open()
} }
const handleTransfer = async (formEl: FormInstance | undefined) => { const handleTransfer = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
await formEl.validate(async (valid) => { await formEl.validate(async (valid) => {
if (valid) { if (valid) {
memberTransfer({ ...formTransfer }).then(res => { memberTransfer({ ...formTransfer }).then(res => {
transferShowDialog.value = false modalTransferApi.close()
loadOrderList() loadOrderList()
}).catch(() => { }).catch(() => {
transferShowDialog.value = false modalTransferApi.close()
loadOrderList() loadOrderList()
}) })
} }
}) })
} }
const transferCancel = () => { modalTransferApi.close() }
/** /**
* 详情 * 详情
* @param data * @param data
*/ */
const cashOutShowDialog = ref(false) const [ModalDetail, modalDetailApi] = useVbenModal()
const cashOutInfo = ref({ const cashOutInfo = ref({
nickname: '', nickname: '',
account_type_name: '', account_type_name: '',
@@ -643,25 +645,26 @@ const cashOutLoading = ref(true)
const detailFn = (id:any) => { const detailFn = (id:any) => {
getCashOutDetail(id).then(res => { getCashOutDetail(id).then(res => {
cashOutInfo.value = res.data cashOutInfo.value = res.data
cashOutShowDialog.value = true modalDetailApi.open()
cashOutLoading.value = false cashOutLoading.value = false
}).catch(() => { }).catch(() => {
loadOrderList() loadOrderList()
}) })
} }
const detailClose = () => { modalDetailApi.close() }
/** /**
* 提现审核 * 提现审核
* @param data * @param data
*/ */
const auditPassShowDialog = ref(false) const [ModalAuditPass, modalAuditPassApi] = useVbenModal()
const curData = ref<any>({}) const curData = ref<any>({})
// 审核成功弹框 // 审核成功弹框
const successfulAuditFn = (data: any) => { const successfulAuditFn = (data: any) => {
curData.value = data curData.value = data
auditPassShowDialog.value = true modalAuditPassApi.open()
} }
const handlePass = () => { const handlePass = () => {
const obj = { const obj = {
@@ -670,24 +673,26 @@ const handlePass = () => {
} }
cashOutAuditFn(obj) cashOutAuditFn(obj)
} }
const auditPassCancel = () => { modalAuditPassApi.close() }
/** /**
* 拒绝审核 * 拒绝审核
*/ */
const auditFailure = ref({ refuse_reason: '', id: 0, action: '' }) const auditFailure = ref({ refuse_reason: '', id: 0, action: '' })
const auditShowDialog = ref(false) const [ModalAuditRefuse, modalAuditRefuseApi] = useVbenModal()
const loading = ref(false) const loading = ref(false)
const auditFailureFn = (data: any) => { const auditFailureFn = (data: any) => {
auditFailure.value.id = data.id auditFailure.value.id = data.id
auditFailure.value.action = 'refuse' auditFailure.value.action = 'refuse'
auditShowDialog.value = true modalAuditRefuseApi.open()
} }
const confirm = () => { const confirm = () => {
auditShowDialog.value = false modalAuditRefuseApi.close()
cashOutAuditFn(auditFailure.value) cashOutAuditFn(auditFailure.value)
} }
const auditRefuseCancel = () => { modalAuditRefuseApi.close() }
const repeat = ref(false) const repeat = ref(false)
const cashOutAuditFn = (data:any) => { const cashOutAuditFn = (data:any) => {
@@ -726,7 +731,7 @@ const memberCancelFn = (data: any) => {
*/ */
const formRemarkRef = ref<FormInstance>() const formRemarkRef = ref<FormInstance>()
const remarkShowDialog = ref(false) const [ModalRemark, modalRemarkApi] = useVbenModal()
const formData = reactive({ const formData = reactive({
id: 0, id: 0,
remark: '' remark: ''
@@ -741,7 +746,7 @@ const formRemarkRules = computed(() => {
const handleRemark = (data: any) => { const handleRemark = (data: any) => {
formData.id = data.id formData.id = data.id
formData.remark = '' formData.remark = ''
remarkShowDialog.value = true modalRemarkApi.open()
} }
const save = async (formEl: FormInstance | undefined) => { const save = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
@@ -749,13 +754,14 @@ const save = async (formEl: FormInstance | undefined) => {
if (valid) { if (valid) {
memberRemark(formData).then((res: any) => { memberRemark(formData).then((res: any) => {
loadOrderList() loadOrderList()
remarkShowDialog.value = false modalRemarkApi.close()
}).catch(() => { }).catch(() => {
remarkShowDialog.value = false modalRemarkApi.close()
}) })
} }
}) })
} }
const remarkCancel = () => { modalRemarkApi.close() }
/** /**
* 会员详情 * 会员详情
*/ */

View File

@@ -25,7 +25,7 @@
</el-table> </el-table>
</div> </div>
<el-dialog v-model="transferDialog" :title="title" width="500px" class="diy-dialog-wrap" :destroy-on-close="true"> <ModalRefund :class="'w-[500px]'" :title="t('transfer')">
<el-form :model="transferFormData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="transferFormData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('transferType')"> <el-form-item :label="t('transferType')">
<el-radio-group v-model="transferFormData.refund_type"> <el-radio-group v-model="transferFormData.refund_type">
@@ -42,11 +42,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="transferDialog = false">{{ t('cancel') }}</el-button> <el-button @click="refundCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalRefund>
</div> </div>
</el-drawer> </el-drawer>
</template> </template>
@@ -57,6 +57,7 @@ import { t } from '@/lang'
import { getPayRefundInfo, getRefundType, getRefundTransfer } from '@/app/api/pay' import { getPayRefundInfo, getRefundType, getRefundTransfer } from '@/app/api/pay'
import { FormInstance } from 'element-plus' import { FormInstance } from 'element-plus'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useVbenModal } from '@vben/common-ui'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -101,9 +102,9 @@ getRefundType().then((data) => {
}) })
}) })
const transferDialog = ref(false) const [ModalRefund, modalRefundApi] = useVbenModal()
const transferEvent = (data:any) => { const transferEvent = (data:any) => {
transferDialog.value = true modalRefundApi.open()
transferFormData.refund_no = data.refund_no transferFormData.refund_no = data.refund_no
transferFormData.refund_money = data.money transferFormData.refund_money = data.money
transferFormData.voucher = '' transferFormData.voucher = ''
@@ -136,17 +137,18 @@ const confirm = async (formEl: FormInstance | undefined) => {
const data = transferFormData const data = transferFormData
getRefundTransfer(data).then(res => { getRefundTransfer(data).then(res => {
loading.value = false loading.value = false
transferDialog.value = false modalRefundApi.close()
refundList.value = [] refundList.value = []
getRefundListInfo(refundNo) getRefundListInfo(refundNo)
emit('loadPayRefundList') emit('loadPayRefundList')
}).catch(() => { }).catch(() => {
transferDialog.value = false modalRefundApi.close()
loading.value = false loading.value = false
}) })
} }
}) })
} }
const refundCancel = () => { modalRefundApi.close() }
defineExpose({ defineExpose({
showDialog, showDialog,
setFormData setFormData

View File

@@ -30,7 +30,7 @@
</el-table> </el-table>
</el-card> </el-card>
<el-dialog v-model="transferDialog" :title="title" width="500px" class="diy-dialog-wrap" :destroy-on-close="true"> <ModalRefund :class="'w-[500px]'" :title="t('transfer')">
<el-form :model="transferFormData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="transferFormData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('transferType')"> <el-form-item :label="t('transferType')">
<el-radio-group v-model="transferFormData.refund_type"> <el-radio-group v-model="transferFormData.refund_type">
@@ -47,11 +47,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="transferDialog = false">{{ t('cancel') }}</el-button> <el-button @click="refundCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalRefund>
</div> </div>
</template> </template>
@@ -62,6 +62,7 @@ import { getPayRefundInfo, getRefundType, getRefundTransfer } from '@/app/api/pa
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { FormInstance } from 'element-plus' import { FormInstance } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue' import { ArrowLeft } from '@element-plus/icons-vue'
import { useVbenModal } from '@vben/common-ui'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -95,9 +96,9 @@ getRefundType().then((data) => {
}) })
}) })
const transferDialog = ref(false) const [ModalRefund, modalRefundApi] = useVbenModal()
const transferEvent = (data:any) => { const transferEvent = (data:any) => {
transferDialog.value = true modalRefundApi.open()
transferFormData.refund_no = data.refund_no transferFormData.refund_no = data.refund_no
transferFormData.refund_money = data.money transferFormData.refund_money = data.money
} }
@@ -130,16 +131,17 @@ const confirm = async (formEl: FormInstance | undefined) => {
const data = transferFormData const data = transferFormData
getRefundTransfer(data).then(res => { getRefundTransfer(data).then(res => {
loading.value = false loading.value = false
transferDialog.value = false modalRefundApi.close()
refundList.value = [] refundList.value = []
setFormData(refundNo) setFormData(refundNo)
}).catch(() => { }).catch(() => {
transferDialog.value = false modalRefundApi.close()
loading.value = false loading.value = false
}) })
} }
}) })
} }
const refundCancel = () => { modalRefundApi.close() }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -207,7 +207,7 @@
</el-empty> </el-empty>
</div> </div>
<el-dialog v-model="authCodeApproveDialog" title="授权码认证" width="400px"> <ModalAuthApprove :class="'w-[400px]'" title="授权码认证">
<el-form :model="formData" label-width="0" ref="formRef" :rules="formRules" class="page-form"> <el-form :model="formData" label-width="0" ref="formRef" :rules="formRules" class="page-form">
<el-card class="box-card !border-none" shadow="never"> <el-card class="box-card !border-none" shadow="never">
<el-form-item prop="auth_code"> <el-form-item prop="auth_code">
@@ -231,9 +231,9 @@
</div> </div>
</el-card> </el-card>
</el-form> </el-form>
</el-dialog> </ModalAuthApprove>
<!-- 详情 --> <!-- 详情 -->
<el-dialog v-model="appStoreShowDialog" :title="t('plugDetail')" width="500px" :destroy-on-close="true"> <ModalAppStoreInfo :class="'w-[500px]'" :title="t('plugDetail')">
<el-form :model="appStoreInfo" label-width="120px" ref="formRef" class="page-form"> <el-form :model="appStoreInfo" label-width="120px" ref="formRef" class="page-form">
<el-form-item :label="t('title')"> <el-form-item :label="t('title')">
<div class="input-width">{{ appStoreInfo.title }}</div> <div class="input-width">{{ appStoreInfo.title }}</div>
@@ -250,13 +250,13 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="appStoreShowDialog = false">{{ t("confirm") }}</el-button> <el-button type="primary" @click="appStoreClose">{{ t("confirm") }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalAppStoreInfo>
<!-- 安装弹窗 --> <!-- 安装弹窗 -->
<el-dialog v-model="installShowDialog" :title="t('addonInstall')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="installShowDialogClose"> <ModalInstall :class="'w-[850px]'" :title="t('addonInstall')" :close-on-click-modal="false" :close-on-press-escape="false" @close="installModalClose">
<el-steps :space="200" :active="installStep" class="number-of-steps" process-status="process" align-center v-if="installStep != 2 && !errorDialog "> <el-steps :space="200" :active="installStep" class="number-of-steps" process-status="process" align-center v-if="installStep != 2 && !errorDialog ">
<el-step :title="t('envCheck')" class="flex-1" /> <el-step :title="t('envCheck')" class="flex-1" />
<el-step :title="t('installProgress')" class="flex-1" /> <el-step :title="t('installProgress')" class="flex-1" />
@@ -369,8 +369,8 @@
</div> </div>
<div class="text-[16px] text-[#9699B6] mt-[10px]" v-if="upgradeDuration>0">本次安装用时{{ formatUpgradeDuration }}</div> <div class="text-[16px] text-[#9699B6] mt-[10px]" v-if="upgradeDuration>0">本次安装用时{{ formatUpgradeDuration }}</div>
<div class="mt-[20px]"> <div class="mt-[20px]">
<el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">返回</el-button> <el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">返回</el-button>
<el-button @click="installShowDialog=false" type="primary" class="!w-[90px]">完成</el-button> <el-button @click="installModalFinish" type="primary" class="!w-[90px]">完成</el-button>
</div> </div>
</template> </template>
</el-result> </el-result>
@@ -385,13 +385,13 @@
{{errorMsg}} {{errorMsg}}
</el-scrollbar> </el-scrollbar>
<el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">错误信息</el-button> <el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">错误信息</el-button>
<el-button @click="installShowDialog=false" type="primary" class="!w-[90px]">完成</el-button> <el-button @click="installModalFinish" type="primary" class="!w-[90px]">完成</el-button>
</template> </template>
</el-result> </el-result>
</div> </div>
</el-dialog> </ModalInstall>
<el-dialog v-model="uninstallShowDialog" :title="t('addonUninstall')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false"> <ModalUninstallCheck :class="'w-[850px]'" :title="t('addonUninstall')" :close-on-click-modal="false" :close-on-press-escape="false">
<el-scrollbar max-height="50vh"> <el-scrollbar max-height="50vh">
<div class="min-h-[150px]"> <div class="min-h-[150px]">
<div class="bg-[#fff] my-3" v-if="uninstallCheckResult.dir"> <div class="bg-[#fff] my-3" v-if="uninstallCheckResult.dir">
@@ -452,18 +452,18 @@
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-dialog> </ModalUninstallCheck>
<!-- 下载提示 --> <!-- 下载提示 -->
<el-dialog v-model="unloadHintDialog" title="下载提示" width="30%"> <ModalUnloadHint :class="'w-[30%]'" title="下载提示">
<span>本地已经存在该插件/应用再次下载会覆盖该插件/应用</span> <span>本地已经存在该插件/应用再次下载会覆盖该插件/应用</span>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="unloadHintDialog = false">取消</el-button> <el-button @click="unloadHintCancel">取消</el-button>
<el-button type="primary" @click="downEventHintFn">确定</el-button> <el-button type="primary" @click="downEventHintFn">确定</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalUnloadHint>
<!-- 更新信息 --> <!-- 更新信息 -->
</el-card> </el-card>
</div> </div>
@@ -760,6 +760,7 @@ const installAddonFn = (key: string) => {
errorDialog.value = false errorDialog.value = false
installType.value = '' installType.value = ''
installShowDialog.value = true installShowDialog.value = true
installModalApi.open()
installAfterTips.value = [] installAfterTips.value = []
installCheckResult.value = res.data installCheckResult.value = res.data
userStore.clearRouters() userStore.clearRouters()
@@ -857,6 +858,7 @@ const formatUpgradeDuration = computed(() => {
const checkInstallTask = () => { const checkInstallTask = () => {
installShowDialog.value = true installShowDialog.value = true
installModalApi.open()
installStep.value = 1 installStep.value = 1
} }
@@ -1063,6 +1065,7 @@ const appStoreInfo = ref({})
const getAddonDetailFn = (data: any) => { const getAddonDetailFn = (data: any) => {
appStoreShowDialog.value = true appStoreShowDialog.value = true
appStoreInfo.value = data appStoreInfo.value = data
appStoreModalApi.open()
} }
// 更新信息 // 更新信息

View File

@@ -116,7 +116,7 @@
</div> </div>
</el-card> </el-card>
<el-dialog v-model="developerDialogVisible" class="developer-dialog-wrap" title="开发人员模式说明" width="30%"> <ModalDeveloperTips :class="'w-[30%] developer-dialog-wrap'" title="开发人员模式说明">
<div> <div>
<p class="text-[16px] mb-[4px]">开发模式</p> <p class="text-[16px] mb-[4px]">开发模式</p>
<div class="text-[14px] indent-[2em]">开发人员模式即软件开发环境指框架开启了开发模式(DEBUG=TRUE) 开发模式时会出现开发选项卡仅用于开发人员使用包括应用及插件的安装卸载系统升级等等本菜单及子项功能均不受系统管理和权限控制</div> <div class="text-[14px] indent-[2em]">开发人员模式即软件开发环境指框架开启了开发模式(DEBUG=TRUE) 开发模式时会出现开发选项卡仅用于开发人员使用包括应用及插件的安装卸载系统升级等等本菜单及子项功能均不受系统管理和权限控制</div>
@@ -125,10 +125,10 @@
</div> </div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="developerDialogVisible = false">确定</el-button> <el-button @click="developerCancel">确定</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalDeveloperTips>
</div> </div>
</template> </template>
@@ -136,12 +136,13 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import useSystemStore from '@/stores/modules/system' import useSystemStore from '@/stores/modules/system'
import { useVbenModal } from '@vben/common-ui'
const systemStore = useSystemStore() const systemStore = useSystemStore()
systemStore.setHeadMenu('') systemStore.setHeadMenu('')
const router = useRouter() const router = useRouter()
const developerDialogVisible = ref(false) const [ModalDeveloperTips, developerModalApi] = useVbenModal()
const toLink = (link:any) => { const toLink = (link:any) => {
router.push(link) router.push(link)
@@ -149,6 +150,7 @@ const toLink = (link:any) => {
const goRouter = () => { const goRouter = () => {
window.open('https://www.niucloud.com/app') window.open('https://www.niucloud.com/app')
} }
const developerCancel = () => { developerModalApi.close() }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true"> <ModalAddMember :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('memberNo')" prop="member_no"> <el-form-item :label="t('memberNo')" prop="member_no">
@@ -25,11 +25,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalAddMember>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -39,6 +39,7 @@ import type { FormInstance } from 'element-plus'
import { addMember, getMemberList, getMemberNo } from '@/app/api/member' import { addMember, getMemberList, getMemberNo } from '@/app/api/member'
import { filterNumber } from '@/utils/common' import { filterNumber } from '@/utils/common'
const [ModalAddMember, addMemberModalApi] = useVbenModal()
const showDialog = ref(false) const showDialog = ref(false)
const loading = ref(false) const loading = ref(false)
const repeat = ref(false) const repeat = ref(false)
@@ -142,6 +143,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
loading.value = false loading.value = false
repeat.value = false repeat.value = false
showDialog.value = false showDialog.value = false
addMemberModalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -175,6 +177,9 @@ defineExpose({
showDialog, showDialog,
setFormData setFormData
}) })
const cancel = () => { showDialog.value = false; addMemberModalApi.close() }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true"> <ModalEditLabel :class="'w-[500px]'" :title="popTitle">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('labelName')" prop="label_name"> <el-form-item :label="t('labelName')" prop="label_name">
<el-input v-model.trim="formData.label_name" clearable :placeholder="t('labelNamePlaceholder')" class="input-width" /> <el-input v-model.trim="formData.label_name" clearable :placeholder="t('labelNamePlaceholder')" class="input-width" />
@@ -15,11 +15,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalEditLabel>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -29,6 +29,7 @@ import type { FormInstance } from 'element-plus'
import { addMemberLabel, updateMemberLabel, getMemberLabelInfo } from '@/app/api/member' import { addMemberLabel, updateMemberLabel, getMemberLabelInfo } from '@/app/api/member'
import { filterNumber } from '@/utils/common' import { filterNumber } from '@/utils/common'
const [ModalEditLabel, editLabelModalApi] = useVbenModal()
const showDialog = ref(false) const showDialog = ref(false)
const loading = ref(false) const loading = ref(false)
const repeat = ref(false) const repeat = ref(false)
@@ -92,6 +93,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
loading.value = false loading.value = false
repeat.value = false repeat.value = false
showDialog.value = false showDialog.value = false
editLabelModalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -122,6 +124,9 @@ defineExpose({
showDialog, showDialog,
setFormData setFormData
}) })
const cancel = () => { showDialog.value = false; editLabelModalApi.close() }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="title || t('updateMember')" width="500px" :destroy-on-close="true"> <ModalEditMember :class="'w-[500px]'" :title="title || t('updateMember')">
<el-form :model="saveData" label-width="90px" :rules="formRules" ref="formRef" class="page-form" @submit.prevent v-loading="loading"> <el-form :model="saveData" label-width="90px" :rules="formRules" ref="formRef" class="page-form" @submit.prevent v-loading="loading">
<el-form-item :label="t('headimg')" v-if="type == 'headimg'"> <el-form-item :label="t('headimg')" v-if="type == 'headimg'">
@@ -43,12 +43,12 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" v-if="method=='batchSet'" @click="batchSetConfirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" v-if="method=='batchSet'" @click="batchSetConfirm(formRef)">{{t('confirm')}}</el-button>
<el-button type="primary" :loading="loading" v-else @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" v-else @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalEditMember>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -64,6 +64,7 @@ const type = ref('')
const title = ref('') const title = ref('')
// 会员id // 会员id
const memberId = ref('') const memberId = ref('')
const [ModalEditMember, editMemberModalApi] = useVbenModal()
const showDialog = ref(false) const showDialog = ref(false)
const loading = ref(false) const loading = ref(false)
const repeat = ref(false) const repeat = ref(false)
@@ -187,6 +188,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
loading.value = false loading.value = false
repeat.value = false repeat.value = false
showDialog.value = false showDialog.value = false
editMemberModalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -265,6 +267,7 @@ const batchSetConfirm = async (formEl: FormInstance | undefined) => {
loading.value = false loading.value = false
repeat.value = false repeat.value = false
showDialog.value = false showDialog.value = false
editMemberModalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -279,6 +282,9 @@ defineExpose({
setDialogType, setDialogType,
batchSetDialogType batchSetDialogType
}) })
const cancel = () => { showDialog.value = false; editMemberModalApi.close() }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
import { useVbenModal } from '@vben/common-ui'

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('adjustBalance')" width="550px" :destroy-on-close="true"> <ModalBalanceEdit :class="'w-[550px]'" :title="t('adjustBalance')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading" @submit.enter.prevent> <el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading" @submit.enter.prevent>
<el-form-item :label="t('currBalance')" > <el-form-item :label="t('currBalance')" >
@@ -25,11 +25,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalBalanceEdit>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -37,8 +37,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { adjustBalance } from '@/app/api/member' import { adjustBalance } from '@/app/api/member'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalBalanceEdit, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
const repeat = ref(false) const repeat = ref(false)
@@ -102,7 +103,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
adjustBalance(data).then(res => { adjustBalance(data).then(res => {
loading.value = false loading.value = false
repeat.value = false repeat.value = false
showDialog.value = false modalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -123,12 +124,11 @@ const setFormData = async (row: any = null) => {
}) })
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('balanceInfo')" width="550px" :destroy-on-close="true"> <ModalBalanceInfo :class="'w-[550px]'" :title="t('balanceInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')"> <el-form-item :label="t('headimg')">
@@ -41,10 +41,10 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalBalanceInfo>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalBalanceInfo, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -98,12 +99,11 @@ const setFormData = async (row: any = null) => {
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('moneyInfo')" width="550px" :destroy-on-close="true"> <ModalCommissionInfo :class="'w-[550px]'" :title="t('moneyInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')"> <el-form-item :label="t('headimg')">
@@ -41,10 +41,10 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalCommissionInfo>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalCommissionInfo, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -98,12 +99,11 @@ const setFormData = async (row: any = null) => {
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('moneyInfo')" width="550px" :destroy-on-close="true"> <ModalMoneyInfo :class="'w-[550px]'" :title="t('moneyInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')" > <el-form-item :label="t('headimg')" >
@@ -37,10 +37,10 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalMoneyInfo>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -48,8 +48,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalMoneyInfo, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -91,12 +92,11 @@ const setFormData = async (row: any = null) => {
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('adjustPoint')" width="550px" :destroy-on-close="true"> <ModalPointEdit :class="'w-[550px]'" :title="t('adjustPoint')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading" @submit.enter.prevent> <el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading" @submit.enter.prevent>
<el-form-item :label="t('currPoint')" > <el-form-item :label="t('currPoint')" >
@@ -25,11 +25,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalPointEdit>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -37,8 +37,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { adjustPoint } from '@/app/api/member' import { adjustPoint } from '@/app/api/member'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalPointEdit, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
const repeat = ref(false) const repeat = ref(false)
@@ -104,7 +105,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
adjustPoint(data).then(res => { adjustPoint(data).then(res => {
loading.value = false loading.value = false
repeat.value = false repeat.value = false
showDialog.value = false modalApi.close()
emit('complete') emit('complete')
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
@@ -126,12 +127,11 @@ const setFormData = async (row: any = null) => {
}) })
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('pointInfo')" width="550px" :destroy-on-close="true"> <ModalPointInfo :class="'w-[550px]'" :title="t('pointInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('headimg')" > <el-form-item :label="t('headimg')" >
@@ -41,10 +41,10 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalPointInfo>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalPointInfo, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -97,12 +98,11 @@ const setFormData = async (row: any = null) => {
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -6,22 +6,15 @@
</el-card> </el-card>
<el-card class="box-card mt-[15px] !border-none" shadow="never" v-loading="loading"> <el-card class="box-card mt-[15px] !border-none" shadow="never" v-loading="loading">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form"> <BaseForm />
<el-form-item :label="t('type')"> <el-form-item :label="t('content')">
<el-input v-model.trim="formData.agreement_key_name" readonly class="input-width" /> <editor v-model="formModel.content" />
</el-form-item> </el-form-item>
<el-form-item :label="t('title')" prop="title">
<el-input v-model.trim="formData.title" clearable :placeholder="t('titlePlaceholder')" class="input-width" maxlength="20" />
</el-form-item>
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
</el-form>
</el-card> </el-card>
<div class="fixed-footer-wrap"> <div class="fixed-footer-wrap">
<div class="fixed-footer"> <div class="fixed-footer">
<el-button type="primary" @click="onSave(formRef)">{{ t('save') }}</el-button> <el-button type="primary" @click="onSave()">{{ t('save') }}</el-button>
<el-button @click="back()">{{ t('cancel') }}</el-button> <el-button @click="back()">{{ t('cancel') }}</el-button>
</div> </div>
</div> </div>
@@ -29,13 +22,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue' import { ArrowLeft } from '@element-plus/icons-vue'
import { getAgreementInfo, editAgreement } from '@/app/api/sys' import { getAgreementInfo, editAgreement } from '@/app/api/sys'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import useTabbarStore from '@/stores/modules/tabbar' import useTabbarStore from '@/stores/modules/tabbar'
import { useVbenForm } from '@/_env/adapter/form'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -48,67 +41,42 @@ const pageName = route.meta.title
* 表单数据 * 表单数据
*/ */
const initialFormData = { const initialFormData = {
agreement_key: '', agreement_key: '',
content: '', content: '',
title: '', title: '',
agreement_key_name: '' agreement_key_name: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
loading.value = true loading.value = true
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
if (!formModel.content || formModel.content.length < 5 || formModel.content.length > 100000) return
loading.value = true
const data = { ...values, content: formModel.content, key: values.agreement_key }
editAgreement(data).then(() => { loading.value = false; back() }).catch(() => { loading.value = false })
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'agreement_key_name', label: t('type'), componentProps: { readonly: true } },
{ component: 'Input', fieldName: 'title', label: t('title'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
})
const setFormData = async (agreement_key: string = '') => { const setFormData = async (agreement_key: string = '') => {
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
const data = await (await getAgreementInfo(agreement_key)).data const data = await (await getAgreementInfo(agreement_key)).data
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
if (data[key] != undefined) formData[key] = data[key] formApi.setModel({ agreement_key_name: formModel.agreement_key_name, title: formModel.title, agreement_key: agreement_key })
}) loading.value = false
loading.value = false
} }
if (agreement_key) setFormData(agreement_key) if (agreement_key) setFormData(agreement_key)
const formRef = ref<FormInstance>() const onSave = async () => { if (loading.value) return; formApi.submit() }
// 表单验证规则 // editor内容的校验已在 handleSubmit 中做长度校验
const formRules = computed(() => {
return {
title: [
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' }
],
content: [
{
required: true,
trigger: ['blur', 'change'],
validator: (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error(t('contentPlaceholder')))
} else if (value.length < 5 || value.length > 100000) {
callback(new Error(t('contentMaxTips')))
return false
} else {
callback()
}
}
}
]
}
})
const onSave = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.key = formData.agreement_key
editAgreement(data).then(res => {
loading.value = false
back()
}).catch(() => {
loading.value = false
})
}
})
}
const back = () => { const back = () => {
tabbarStore.removeTab(route.path) tabbarStore.removeTab(route.path)

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('cronInfo')" width="550px" :destroy-on-close="true"> <ModalCronInfo :class="'w-[550px]'" :title="t('cronInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('title')" > <el-form-item :label="t('title')" >
@@ -47,18 +47,19 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button> <el-button type="primary" @click="cancel">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalCronInfo>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [ModalCronInfo, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -100,12 +101,11 @@ const setFormData = async (row: any = null) => {
}) })
} }
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog, defineExpose({ setFormData })
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,51 +1,40 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('messageInfo')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="t('messageInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <div class="page-form" v-loading="loading">
<el-form-item :label="t('messageKey')">
<el-form-item :label="t('messageKey')"> <div class="input-width"> {{ formModel.name }} </div>
<div class="input-width"> {{ formData.name }} </div> </el-form-item>
</el-form-item> <el-form-item :label="t('smsType')">
<div class="input-width">
<el-form-item :label="t('smsType')"> <div v-if="formModel.notice_type == 'sms'">{{ t('sms') }}</div>
<div class="input-width"> <div v-if="formModel.notice_type == 'wechat'">{{ t('wechat') }}</div>
<div v-if="formData.notice_type == 'sms'">{{ t('sms') }}</div> <div v-if="formModel.notice_type == 'weapp'">{{ t('weapp') }}</div>
<div v-if="formData.notice_type == 'wechat'">{{ t('wechat') }}</div> </div>
<div v-if="formData.notice_type == 'weapp'">{{ t('weapp') }}</div> </el-form-item>
</div> <el-form-item :label="t('nickname')">
</el-form-item> <div class="input-width"> {{ formModel.nickname }} </div>
</el-form-item>
<!-- <el-form-item :label="t('messageData')"> <el-form-item :label="t('receiver')">
<div class="input-width"> {{ formData.message_data }} </div> <div class="input-width"> {{ formModel.receiver }} </div>
</el-form-item> --> </el-form-item>
<el-form-item :label="t('createTime')">
<el-form-item :label="t('nickname')"> <div class="input-width"> {{ formModel.create_time }} </div>
<div class="input-width"> {{ formData.nickname }} </div> </el-form-item>
</el-form-item> </div>
<template #footer>
<el-form-item :label="t('receiver')"> <span class="dialog-footer">
<div class="input-width"> {{ formData.receiver }} </div> <el-button type="primary" @click="close()">{{ t('confirm') }}</el-button>
</el-form-item> </span>
</template>
<el-form-item :label="t('createTime')"> </Modal>
<div class="input-width"> {{ formData.create_time }} </div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -61,34 +50,21 @@ const initialFormData = {
receiver: '', receiver: '',
notice_type: '' notice_type: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
}
})
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (row) {
if (row) { Object.keys(formModel).forEach((key: string) => { if (row[key] != undefined) formModel[key] = row[key] })
Object.keys(formData).forEach((key: string) => { }
if (row[key] != undefined) formData[key] = row[key] loading.value = false
}) modalApi.open()
}
loading.value = false
} }
defineExpose({ const close = () => { modalApi.close() }
showDialog,
setFormData defineExpose({ setFormData })
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,121 +1,82 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="t('noticeSetting')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('status')"> <el-form-item :label="t('name')">
<el-radio-group v-model="formData.is_sms"> <div class="input-width"> {{ formModel.name }} </div>
<el-radio :label="1">{{ t('startUsing') }}</el-radio> </el-form-item>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio> <el-form-item :label="t('title')">
</el-radio-group> <div class="input-width"> {{ formModel.title }} </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('smsContent')">
<el-form-item :label="t('name')"> <div class="input-width"> {{ formModel.content }} </div>
<div class="input-width"> {{ formData.name }} </div> </el-form-item>
</el-form-item> <template #footer>
<span class="dialog-footer">
<el-form-item :label="t('title')"> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<div class="input-width"> {{ formData.title }} </div> <el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</el-form-item> </span>
</template>
<el-form-item :label="t('smsId')" prop="sms_id"> </Modal>
<el-input v-model.trim="formData.sms_id" :placeholder="t('smsIdPlaceholder')" class="input-width" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('smsContent')">
<div class="input-width"> {{ formData.content }} </div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { editNotice } from '@/app/api/notice' import { editNotice } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
* 表单数据 * 表单数据
*/ */
const initialFormData = { const initialFormData = {
is_sms: 0, is_sms: 0,
key: '', key: '',
name: '', name: '',
sms_default_content: '', sms_default_content: '',
title: '', title: '',
type: '', type: '',
sms_id: '', sms_id: '',
content: '' content: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
const formRef = ref<FormInstance>() commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
// 表单验证规则 loading.value = true
const formRules = computed(() => { const data = { ...values, status: values.is_sms }
return { editNotice(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
sms_id: [ },
{ required: true, message: t('smsIdPlaceholder'), trigger: 'blur' } layout: 'horizontal',
] schema: [
} { component: 'RadioGroup', fieldName: 'is_sms', label: t('status'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
{ component: 'Input', fieldName: 'sms_id', label: t('smsId'), rules: [{ required: true }] }
],
wrapperClass: 'grid-cols-1'
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
const confirm = async () => { if (loading.value) return; formApi.submit() }
/** const cancel = () => { modalApi.close() }
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.status = data.is_sms
editNotice(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
// showDialog.value = false
})
}
})
}
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (row) {
if (row) { Object.keys(formModel).forEach((key: string) => {
Object.keys(formData).forEach((key: string) => { if (row[key] != undefined) formModel[key] = row[key]
if (row[key] != undefined) formData[key] = row[key] if (row.sms && row.sms[key] != undefined) formModel[key] = row.sms[key]
if (row.sms && row.sms[key] != undefined) formData[key] = row.sms[key] })
}) }
} formApi.setModel({ ...formModel })
loading.value = false
loading.value = false modalApi.open()
} }
defineExpose({ defineExpose({ setFormData })
showDialog,
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,45 +1,34 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="t('noticeSetting')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('status')"> <el-form-item :label="t('name')">
<el-radio-group v-model="formData.is_weapp"> <div class="input-width"> {{ formModel.name }} </div>
<el-radio :label="1">{{ t('startUsing') }}</el-radio> </el-form-item>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio> <el-form-item :label="t('weappTempKey')">
</el-radio-group> <div class="input-width"> {{ formModel.tid }} </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('content')">
<el-form-item :label="t('name')"> <div class="input-width">
<div class="input-width"> {{ formData.name }} </div> <div v-for="(item, index) in formModel.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
</el-form-item> </div>
</el-form-item>
<el-form-item :label="t('weappTempKey')">
<div class="input-width"> {{ formData.tid }} </div>
</el-form-item>
<el-form-item :label="t('content')">
<div class="input-width">
<div v-for="(item, index) in formData.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
</div>
</el-form-item>
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { editNoticeStatus } from '@/app/api/notice' import { editNoticeStatus } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -52,66 +41,45 @@ const initialFormData = {
title: '', title: '',
type: '', type: '',
content: [], content: [],
first: '',
remark: '',
tid: '' tid: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>() const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
// 表单验证规则 handleSubmit: async (values) => {
const formRules = computed(() => { loading.value = true
return { const data = { ...values, status: values.is_weapp }
editNoticeStatus(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
} },
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_weapp', label: t('status'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } }
],
wrapperClass: 'grid-cols-1'
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
const confirm = async () => { if (loading.value) return; formApi.submit() }
/** const cancel = () => { modalApi.close() }
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.status = data.is_weapp
editNoticeStatus(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (row) { if (row) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (row[key] != undefined) formData[key] = row[key] if (row[key] != undefined) formModel[key] = row[key]
if (row.weapp && row.weapp[key] != undefined) formData[key] = row.weapp[key] if (row.weapp && row.weapp[key] != undefined) formModel[key] = row.weapp[key]
}) })
} }
formApi.setModel({ ...formModel })
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ defineExpose({ setFormData })
showDialog,
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,57 +1,37 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="t('noticeSetting')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('status')"> <el-form-item :label="t('name')">
<el-radio-group v-model="formData.is_wechat"> <div class="input-width">{{ formModel.name }} </div>
<el-radio :label="1">{{ t('startUsing') }}</el-radio> </el-form-item>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio> <el-form-item :label="t('tempKey')">
</el-radio-group> <div class="input-width">{{ formModel.temp_key }} </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('keywordNameList')">
<el-form-item :label="t('name')"> <div class="input-width">{{ formModel.keyword_name_list ? formModel.keyword_name_list.join('') : '' }} </div>
<div class="input-width">{{ formData.name }} </div> </el-form-item>
</el-form-item> <el-form-item :label="t('content')">
<div class="input-width">
<el-form-item :label="t('tempKey')"> <div v-for="(item, index) in formModel.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
<div class="input-width">{{ formData.temp_key }} </div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('keywordNameList')">
<div class="input-width">{{ formData.keyword_name_list ? formData.keyword_name_list.join('') : '' }} </div>
</el-form-item>
<!-- <el-form-item :label="t('first')" prop="first">-->
<!-- <el-input v-model.trim="formData.wechat_first" :placeholder="t('firstPlaceholder')" class="input-width" show-word-limit clearable />-->
<!-- </el-form-item>-->
<el-form-item :label="t('content')">
<div class="input-width">
<div v-for="(item, index) in formData.content" :key="index">{{ item[0] }}{{ item[1] }} </div>
</div>
</el-form-item>
<!-- <el-form-item :label="t('remark')" prop="remark">-->
<!-- <el-input v-model.trim="formData.wechat_remark" :placeholder="t('remarkPlaceholder')" class="input-width" show-word-limit clearable />-->
<!-- </el-form-item>-->
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { editNotice } from '@/app/api/notice' import { editNotice } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -64,69 +44,45 @@ const initialFormData = {
title: '', title: '',
type: '', type: '',
content: [], content: [],
// first: '',
// remark: '',
temp_key: '', temp_key: '',
keyword_name_list: '' keyword_name_list: ''
// wechat_first: '',
// wechat_remark: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>() const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
// 表单验证规则 handleSubmit: async (values) => {
const formRules = computed(() => { loading.value = true
return { const data = { ...values, status: values.is_wechat }
editNotice(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
} },
layout: 'horizontal',
schema: [
{ component: 'RadioGroup', fieldName: 'is_wechat', label: t('status'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } }
],
wrapperClass: 'grid-cols-1'
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
const confirm = async () => { if (loading.value) return; formApi.submit() }
/** const cancel = () => { modalApi.close() }
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
data.status = data.is_wechat
editNotice(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
})
}
})
}
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (row) { if (row) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (row[key] != undefined) formData[key] = row[key] if (row[key] != undefined) formModel[key] = row[key]
if (row.wechat && row.wechat[key] != undefined) formData[key] = row.wechat[key] if (row.wechat && row.wechat[key] != undefined) formModel[key] = row.wechat[key]
}) })
// if (!row.wechat_first) formData['wechat_first'] = row['first']
// if (!row.wechat_remark) formData['wechat_remark'] = row['remark']
} }
formApi.setModel({ ...formModel })
loading.value = false loading.value = false
modalApi.open()
} }
defineExpose({ defineExpose({ setFormData })
showDialog,
setFormData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('updateAlipay')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="t('updateAlipay')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('appId')" prop="config.app_id"> <el-form-item :label="t('appId')" prop="config.app_id">
<el-input v-model.trim="formData.config.app_id" :placeholder="t('appIdPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable /> <el-input v-model.trim="formData.config.app_id" :placeholder="t('appIdPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable />
@@ -10,43 +10,42 @@
<el-input v-model.trim="formData.config.app_secret_cert" :placeholder="t('appSecretCertPlaceholder')" class="input-width" type="textarea" rows="4" clearable /> <el-input v-model.trim="formData.config.app_secret_cert" :placeholder="t('appSecretCertPlaceholder')" class="input-width" type="textarea" rows="4" clearable />
</el-form-item> </el-form-item>
<el-form-item :label="t('appPublicCertPath')" prop="config.app_public_cert_path"> <el-form-item :label="t('appPublicCertPath')">
<div class="input-width"> <div class="input-width">
<upload-file v-model.trim="formData.config.app_public_cert_path" api="sys/document/aliyun" /> <upload-file v-model.trim="formModel.config.app_public_cert_path" api="sys/document/aliyun" />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('alipayPublicCertPath')" prop="config.alipay_public_cert_path"> <el-form-item :label="t('alipayPublicCertPath')">
<div class="input-width"> <div class="input-width">
<upload-file v-model="formData.config.alipay_public_cert_path" api="sys/document/aliyun" /> <upload-file v-model="formModel.config.alipay_public_cert_path" api="sys/document/aliyun" />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('alipayRootCertPath')" prop="config.alipay_root_cert_path"> <el-form-item :label="t('alipayRootCertPath')">
<div class="input-width"> <div class="input-width">
<upload-file v-model="formData.config.alipay_root_cert_path" api="sys/document/aliyun" /> <upload-file v-model="formModel.config.alipay_root_cert_path" api="sys/document/aliyun" />
</div> </div>
</el-form-item> </el-form-item>
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="cancel">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import Test from '@/utils/test' import Test from '@/utils/test'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
const initData = ref<any>(null) const initData = ref<any>(null)
/** /**
@@ -65,29 +64,22 @@ const initialFormData = {
status: 0, status: 0,
is_default: 0 is_default: 0
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
const formRef = ref<FormInstance>() commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
// 表单验证规则 loading.value = true
const formRules = computed(() => { const merged = { ...values, config: { ...formModel.config } }
return { emit('complete', merged)
'config.app_id': [ loading.value = false
{ required: true, message: t('appIdPlaceholder'), trigger: 'blur' } modalApi.close()
], },
'config.app_secret_cert': [ layout: 'horizontal',
{ required: true, message: t('appSecretCertPlaceholder'), trigger: 'blur' } schema: [
], { component: 'Input', fieldName: 'config.app_id', label: t('appId'), rules: [{ required: true }] },
'config.app_public_cert_path': [ { component: 'Textarea', fieldName: 'config.app_secret_cert', label: t('appSecretCert'), rules: [{ required: true }], componentProps: { rows: 4 } }
{ required: true, message: t('appPublicCertPathPlaceholder'), trigger: 'blur' } ],
], wrapperClass: 'grid-cols-1'
'config.alipay_public_cert_path': [
{ required: true, message: t('alipayPublicCertPathPlaceholder'), trigger: 'blur' }
],
'config.alipay_root_cert_path': [
{ required: true, message: t('alipayRootCertPathPlaceholder'), trigger: 'blur' }
]
}
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
@@ -96,50 +88,43 @@ const emit = defineEmits(['complete'])
* 确认 * 确认
* @param formEl * @param formEl
*/ */
const confirm = async (formEl: FormInstance | undefined) => { const confirm = async () => { if (loading.value) return; formApi.submit() }
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const cancel = () => { const cancel = () => {
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (initData.value) { if (initData.value) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (initData.value[key] != undefined) formData[key] = initData.value[key] if (initData.value[key] != undefined) formModel[key] = initData.value[key]
}) })
formData.channel = initData.value.redio_key.split('_')[0] formModel.channel = initData.value.redio_key.split('_')[0]
formData.status = Number(formData.status) formModel.status = Number(formModel.status)
} }
emit('complete', formData) emit('complete', formModel)
showDialog.value = false modalApi.close()
} }
const setFormData = async (data: any = null) => { const setFormData = async (data: any = null) => {
initData.value = cloneDeep(data) initData.value = cloneDeep(data)
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (data) { if (data) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key] if (data[key] != undefined) formModel[key] = data[key]
}) })
formData.channel = data.redio_key.split('_')[0] formModel.channel = data.redio_key.split('_')[0]
formData.status = Number(formData.status) formModel.status = Number(formModel.status)
} }
formApi.setModel({ ...formModel })
loading.value = false loading.value = false
modalApi.open()
} }
const enableVerify = () => { const enableVerify = () => {
let verify = true let verify = true
if (Test.empty(formData.config.app_id) || Test.empty(formData.config.app_secret_cert) || Test.empty(formData.config.app_public_cert_path) || Test.empty(formData.config.alipay_public_cert_path) || Test.empty(formData.config.alipay_root_cert_path)) verify = false if (Test.empty(formModel.config.app_id) || Test.empty(formModel.config.app_secret_cert) || Test.empty(formModel.config.app_public_cert_path) || Test.empty(formModel.config.alipay_public_cert_path) || Test.empty(formModel.config.alipay_root_cert_path)) verify = false
return verify return verify
} }
defineExpose({ defineExpose({
showDialog,
setFormData, setFormData,
enableVerify enableVerify
}) })

View File

@@ -1,58 +1,59 @@
<template> <template>
<el-dialog v-model="showDialog" :title="formData.config.pay_type_name ? formData.config.pay_type_name : t('updateFriendsPay')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="formModel.config.pay_type_name ? formModel.config.pay_type_name : t('updateFriendsPay')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('friendsPaySwitch')"> <el-form-item :label="t('friendsPaySwitch')">
<el-switch v-model="formData.config.pay_explain_switch" :active-value="1" :inactive-value="0"/> <el-switch v-model="formModel.config.pay_explain_switch" :active-value="1" :inactive-value="0"/>
</el-form-item> </el-form-item>
<template v-if="formData.config.pay_explain_switch == 1"> <template v-if="formModel.config.pay_explain_switch == 1">
<el-form-item :label="t('friendsPayTitle')" prop="config.pay_explain_title"> <el-form-item :label="t('friendsPayTitle')">
<el-input v-model.trim="formData.config.pay_explain_title" :placeholder="t('friendsPayTitlePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable /> <el-input v-model.trim="formModel.config.pay_explain_title" :placeholder="t('friendsPayTitlePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item> </el-form-item>
<el-form-item :label="t('desContent')" prop="config.pay_explain_content"> <el-form-item :label="t('desContent')">
<el-input v-model.trim="formData.config.pay_explain_content" :placeholder="t('desContentPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="120" show-word-limit clearable /> <el-input v-model.trim="formModel.config.pay_explain_content" :placeholder="t('desContentPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="120" show-word-limit clearable />
</el-form-item> </el-form-item>
</template> </template>
<el-form-item :label="t('friendsPayGoodsSwitch')"> <el-form-item :label="t('friendsPayGoodsSwitch')">
<div> <div>
<el-switch v-model="formData.config.pay_info_switch" :active-value="1" :inactive-value="0"/> <el-switch v-model="formModel.config.pay_info_switch" :active-value="1" :inactive-value="0"/>
<div class="text-[12px] text-[#999] leading-[20px]">{{ t('friendsPayGoodsSwitchTips') }}</div> <div class="text-[12px] text-[#999] leading-[20px]">{{ t('friendsPayGoodsSwitchTips') }}</div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('friendsPayName')" prop="config.pay_type_name"> <el-form-item :label="t('friendsPayName')">
<el-input v-model.trim="formData.config.pay_type_name" :placeholder="t('friendsPayNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable /> <el-input v-model.trim="formModel.config.pay_type_name" :placeholder="t('friendsPayNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item> </el-form-item>
<el-form-item :label="t('helpName')" prop="config.pay_page_name"> <el-form-item :label="t('helpName')">
<el-input v-model.trim="formData.config.pay_page_name" :placeholder="t('helpNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable /> <el-input v-model.trim="formModel.config.pay_page_name" :placeholder="t('helpNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item> </el-form-item>
<el-form-item :label="t('helpBtn')" prop="config.pay_button_name"> <el-form-item :label="t('helpBtn')">
<el-input v-model.trim="formData.config.pay_button_name" :placeholder="t('helpBtnPlaceholder')" class="input-width" maxlength="10" show-word-limit clearable /> <el-input v-model.trim="formModel.config.pay_button_name" :placeholder="t('helpBtnPlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
</el-form-item> </el-form-item>
<el-form-item :label="t('remark')" prop="config.pay_leave_message"> <el-form-item :label="t('remark')">
<el-input v-model.trim="formData.config.pay_leave_message" :placeholder="t('remarkPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="20" show-word-limit clearable /> <el-input v-model.trim="formModel.config.pay_leave_message" :placeholder="t('remarkPlaceholder')" class="input-width" type="textarea" rows="4" maxlength="20" show-word-limit clearable />
</el-form-item> </el-form-item>
<el-form-item :label="t('payWechatImage')" prop="config.pay_wechat_share_image" v-if="initData.redio_key == 'wechat_friendspay'"> <el-form-item :label="t('payWechatImage')" v-if="initData.redio_key == 'wechat_friendspay'">
<upload-image v-model="formData.config.pay_wechat_share_image" :limit="1" /> <upload-image v-model="formModel.config.pay_wechat_share_image" :limit="1" />
</el-form-item> </el-form-item>
<el-form-item :label="t('payWeappImage')" prop="config.pay_weapp_share_image" v-if="initData.redio_key == 'weapp_friendspay'"> <el-form-item :label="t('payWeappImage')" v-if="initData.redio_key == 'weapp_friendspay'">
<upload-image v-model="formData.config.pay_weapp_share_image" :limit="1" /> <upload-image v-model="formModel.config.pay_weapp_share_image" :limit="1" />
</el-form-item> </el-form-item>
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="cancel()">{{ t('cancel') }}</el-button> <el-button @click="cancel()">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import Test from '@/utils/test' import Test from '@/utils/test'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
const initData = ref<any>(null) const initData = ref<any>(null)
/** /**
@@ -77,52 +78,23 @@ const initialFormData = {
status: 0, status: 0,
is_default: 0 is_default: 0
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
const formRef = ref<FormInstance>() commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
// 表单验证规则 loading.value = true
const formRules = computed(() => { emit('complete', { ...values, config: { ...formModel.config } })
return { loading.value = false
'config.pay_explain_title': [ modalApi.close()
{ required: true, message: t('friendsPayTitlePlaceholder'), trigger: 'blur' }, },
{ layout: 'horizontal',
validator: (rule: any, value: string, callback: any) => { schema: [
if (formData.config.pay_explain_switch == 1 && value === '') { { component: 'Input', fieldName: 'config.pay_type_name', label: t('friendsPayName'), rules: [{ required: true }] },
callback(new Error(t('friendsPayTitlePlaceholder'))) { component: 'Input', fieldName: 'config.pay_page_name', label: t('helpName'), rules: [{ required: true }] },
} { component: 'Input', fieldName: 'config.pay_button_name', label: t('helpBtn'), rules: [{ required: true }] },
{ component: 'Textarea', fieldName: 'config.pay_leave_message', label: t('remark'), rules: [{ required: true }] }
callback() ],
}, wrapperClass: 'grid-cols-1'
trigger: 'blur'
}
],
'config.pay_explain_content': [
{ required: true, message: t('desContentPlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: string, callback: any) => {
if (formData.config.pay_explain_switch == 1 && value === '') {
callback(new Error(t('desContentPlaceholder')))
}
callback()
},
trigger: 'blur'
}
],
'config.pay_type_name': [
{ required: true, message: t('friendsPayNamePlaceholder'), trigger: 'blur' }
],
'config.pay_page_name': [
{ required: true, message: t('helpNamePlaceholder'), trigger: 'blur' }
],
'config.pay_button_name': [
{ required: true, message: t('helpBtnPlaceholder'), trigger: 'blur' }
],
'config.pay_leave_message': [
{ required: true, message: t('remarkPlaceholder'), trigger: 'blur' }
]
}
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
@@ -130,51 +102,40 @@ const emit = defineEmits(['complete'])
* 确认 * 确认
* @param formEl * @param formEl
*/ */
const confirm = async (formEl: FormInstance | undefined) => { const confirm = async () => { if (loading.value) return; formApi.submit() }
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const cancel = () => { const cancel = () => {
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (initData.value) { if (initData.value) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => { if (initData.value[key] != undefined) formModel[key] = initData.value[key] })
if (initData.value[key] != undefined) formData[key] = initData.value[key] formModel.channel = initData.value.redio_key.split('_')[0]
}) formModel.status = Number(formModel.status)
formData.channel = initData.value.redio_key.split('_')[0] }
formData.status = Number(formData.status) emit('complete', formModel)
} modalApi.close()
emit('complete', formData)
showDialog.value = false
} }
const setFormData = async (data: any = null) => { const setFormData = async (data: any = null) => {
initData.value = cloneDeep(data) initData.value = cloneDeep(data)
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (data) { if (data) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
if (data[key] != undefined) formData[key] = data[key] formModel.channel = data.redio_key.split('_')[0]
}) formModel.status = Number(formModel.status)
formData.channel = data.redio_key.split('_')[0] }
formData.status = Number(formData.status) formApi.setModel({ ...formModel })
} loading.value = false
loading.value = false modalApi.open()
} }
const enableVerify = () => { const enableVerify = () => {
let verify = true let verify = true
if ((formData.config.pay_explain_switch == 1 && Test.empty(formData.config.pay_explain_title)) || (formData.config.pay_explain_switch == 1 && Test.empty(formData.config.pay_explain_content)) || Test.empty(formData.config.pay_type_name) || Test.empty(formData.config.pay_page_name) || Test.empty(formData.config.pay_button_name) || Test.empty(formData.config.pay_leave_message)) verify = false if ((formModel.config.pay_explain_switch == 1 && Test.empty(formModel.config.pay_explain_title)) || (formModel.config.pay_explain_switch == 1 && Test.empty(formModel.config.pay_explain_content)) || Test.empty(formModel.config.pay_type_name) || Test.empty(formModel.config.pay_page_name) || Test.empty(formModel.config.pay_button_name) || Test.empty(formModel.config.pay_leave_message)) verify = false
return verify return verify
} }
defineExpose({ defineExpose({
showDialog,
setFormData, setFormData,
enableVerify enableVerify
}) })

View File

@@ -1,6 +1,6 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('updateOfflinepay')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="t('updateOfflinepay')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('collectionName')" prop="config.collection_name"> <el-form-item :label="t('collectionName')" prop="config.collection_name">
<el-input v-model.trim="formData.config.collection_name" :placeholder="t('collectionNamePlaceholder')" class="input-width" show-word-limit clearable /> <el-input v-model.trim="formData.config.collection_name" :placeholder="t('collectionNamePlaceholder')" class="input-width" show-word-limit clearable />
@@ -17,23 +17,22 @@
<el-form-item :label="t('collectionDesc')" prop="config.collection_desc"> <el-form-item :label="t('collectionDesc')" prop="config.collection_desc">
<el-input v-model.trim="formData.config.collection_desc" :placeholder="t('collectionDescPlaceholder')" class="input-width" type="textarea" rows="4" clearable /> <el-input v-model.trim="formData.config.collection_desc" :placeholder="t('collectionDescPlaceholder')" class="input-width" type="textarea" rows="4" clearable />
</el-form-item> </el-form-item>
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel()">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -51,26 +50,24 @@ const initialFormData = {
status: 0, status: 0,
is_default: 0 is_default: 0
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
const formRef = ref<FormInstance>() commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
// 表单验证规则 loading.value = true
const formRules = computed(() => { const merged = { ...values }
return { emit('complete', merged)
'config.collection_name': [ loading.value = false
{ required: true, message: t('collectionNamePlaceholder'), trigger: 'blur' } modalApi.close()
], },
'config.collection_bank': [ layout: 'horizontal',
{ required: true, message: t('collectionBankPlaceholder'), trigger: 'blur' } schema: [
], { component: 'Input', fieldName: 'config.collection_name', label: t('collectionName'), rules: [{ required: true }] },
'config.collection_account': [ { component: 'Input', fieldName: 'config.collection_bank', label: t('collectionBank'), rules: [{ required: true }] },
{ required: true, message: t('collectionAccountPlaceholder'), trigger: 'blur' } { component: 'Input', fieldName: 'config.collection_account', label: t('collectionAccount'), rules: [{ required: true }] },
], { component: 'Textarea', fieldName: 'config.collection_desc', label: t('collectionDesc'), rules: [{ required: true }], componentProps: { rows: 4 } }
'config.collection_desc': [ ],
{ required: true, message: t('collectionDescPlaceholder'), trigger: 'blur' } wrapperClass: 'grid-cols-1'
]
}
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
@@ -79,33 +76,24 @@ const emit = defineEmits(['complete'])
* 确认 * 确认
* @param formEl * @param formEl
*/ */
const confirm = async (formEl: FormInstance | undefined) => { const confirm = async () => { if (loading.value) return; formApi.submit() }
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const setFormData = async (data: any = null) => { const setFormData = async (data: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (data) { if (data) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
if (data[key] != undefined) formData[key] = data[key] formModel.channel = data.redio_key.split('_')[0]
}) formModel.status = Number(formModel.status)
formData.channel = data.redio_key.split('_')[0] }
formData.status = Number(formData.status) formApi.setModel({ ...formModel })
} loading.value = false
loading.value = false modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog,
setFormData defineExpose({ setFormData })
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('updateWechat')" width="500px" :destroy-on-close="true"> <Modal :class="'w-[500px]'" :title="t('updateWechat')">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('mchId')" prop="config.mch_id"> <el-form-item :label="t('mchId')" prop="config.mch_id">
<el-input v-model.trim="formData.config.mch_id" :placeholder="t('mchIdPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable /> <el-input v-model.trim="formData.config.mch_id" :placeholder="t('mchIdPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable />
@@ -12,33 +12,33 @@
<div class="form-tip">{{ t('mchSecretKeyTips') }}</div> <div class="form-tip">{{ t('mchSecretKeyTips') }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('mchSecretCert')" prop="config.mch_secret_cert"> <el-form-item :label="t('mchSecretCert')">
<div class="input-width"> <div class="input-width">
<upload-file v-model="formData.config.mch_secret_cert" api="sys/document/wechat" /> <upload-file v-model="formModel.config.mch_secret_cert" api="sys/document/wechat" />
</div> </div>
<div class="form-tip">{{ t('mchSecretCertTips') }}</div> <div class="form-tip">{{ t('mchSecretCertTips') }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('mchPublicCertPath')" prop="config.mch_public_cert_path"> <el-form-item :label="t('mchPublicCertPath')">
<div class="input-width"> <div class="input-width">
<upload-file v-model="formData.config.mch_public_cert_path" api="sys/document/wechat" /> <upload-file v-model="formModel.config.mch_public_cert_path" api="sys/document/wechat" />
</div> </div>
<div class="form-tip">{{ t('mchPublicCertPathTips') }}</div> <div class="form-tip">{{ t('mchPublicCertPathTips') }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('wechatpayPublicCert')" prop="config.wechat_public_cert_path"> <el-form-item :label="t('wechatpayPublicCert')">
<div class="input-width"> <div class="input-width">
<upload-file v-model="formData.config.wechat_public_cert_path" api="sys/document/wechat" /> <upload-file v-model="formModel.config.wechat_public_cert_path" api="sys/document/wechat" />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('wechatpayPublicCertId')" prop="config.wechat_public_cert_id"> <el-form-item :label="t('wechatpayPublicCertId')">
<div class="input-width"> <div class="input-width">
<el-input v-model.trim="formData.config.wechat_public_cert_id" placeholder="" class="input-width" show-word-limit clearable /> <el-input v-model.trim="formModel.config.wechat_public_cert_id" placeholder="" class="input-width" show-word-limit clearable />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="t('jsapiDir')" v-show="formData.channel == 'wechat' || formData.channel == 'weapp'"> <el-form-item :label="t('jsapiDir')" v-show="formModel.channel == 'wechat' || formModel.channel == 'weapp'">
<el-input :model-value="wapDomain + '/'" placeholder="Please input" class="input-width" :readonly="true" :disabled="true"> <el-input :model-value="wapDomain + '/'" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
<template #append> <template #append>
<div class="cursor-pointer" @click="copyEvent(wapDomain + '/')">{{ t('copy') }} <div class="cursor-pointer" @click="copyEvent(wapDomain + '/')">{{ t('copy') }}
@@ -48,7 +48,7 @@
<div class="form-tip !leading-normal">{{ t('jsapiDirTips') }}</div> <div class="form-tip !leading-normal">{{ t('jsapiDirTips') }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('h5Domain')" v-show="formData.channel == 'h5'"> <el-form-item :label="t('h5Domain')" v-show="formModel.channel == 'h5'">
<el-input :model-value="wapDomain.replace('http://', '').replace('https://', '')" placeholder="Please input" class="input-width" :readonly="true" :disabled="true"> <el-input :model-value="wapDomain.replace('http://', '').replace('https://', '')" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
<template #append> <template #append>
<div class="cursor-pointer" @click="copyEvent(wapDomain.replace('http://', '').replace('https://', ''))">{{ t('copy') }} <div class="cursor-pointer" @click="copyEvent(wapDomain.replace('http://', '').replace('https://', ''))">{{ t('copy') }}
@@ -58,7 +58,7 @@
<div class="form-tip !leading-normal">{{ t('h5DomainTips') }}</div> <div class="form-tip !leading-normal">{{ t('h5DomainTips') }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('nativeDomain')" v-show="formData.channel == 'pc'"> <el-form-item :label="t('nativeDomain')" v-show="formModel.channel == 'pc'">
<el-input :model-value="serviceDomain" placeholder="Please input" class="input-width" :readonly="true" :disabled="true"> <el-input :model-value="serviceDomain" placeholder="Please input" class="input-width" :readonly="true" :disabled="true">
<template #append> <template #append>
<div class="cursor-pointer" @click="copyEvent(serviceDomain)">{{ t('copy') }} <div class="cursor-pointer" @click="copyEvent(serviceDomain)">{{ t('copy') }}
@@ -67,27 +67,28 @@
</el-input> </el-input>
<div class="form-tip !leading-normal">{{ t('nativeDomainTips') }}</div> <div class="form-tip !leading-normal">{{ t('nativeDomainTips') }}</div>
</el-form-item> </el-form-item>
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="cancel">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue' import { ref, reactive, watch } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import { FormInstance, ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import Test from '@/utils/test' import Test from '@/utils/test'
import { getUrl } from '@/app/api/sys' import { getUrl } from '@/app/api/sys'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
const wapDomain = ref('') const wapDomain = ref('')
const serviceDomain = ref('') const serviceDomain = ref('')
@@ -115,26 +116,22 @@ const initialFormData = {
status: 0, status: 0,
is_default: 0 is_default: 0
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
const formRef = ref<FormInstance>() commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
// 表单验证规则 loading.value = true
const formRules = computed(() => { const merged = { ...values, config: { ...formModel.config } }
return { emit('complete', merged)
'config.mch_id': [ loading.value = false
{ required: true, message: t('mchIdPlaceholder'), trigger: 'blur' } modalApi.close()
], },
'config.mch_secret_key': [ layout: 'horizontal',
{ required: true, message: t('mchSecretKeyPlaceholder'), trigger: 'blur' } schema: [
], { component: 'Input', fieldName: 'config.mch_id', label: t('mchId'), rules: [{ required: true }] },
'config.mch_secret_cert': [ { component: 'Input', fieldName: 'config.mch_secret_key', label: t('mchSecretKey'), rules: [{ required: true }] }
{ required: true, message: t('mchSecretCertPlaceholder'), trigger: 'blur' } ],
], wrapperClass: 'grid-cols-1'
'config.mch_public_cert_path': [
{ required: true, message: t('mchPublicCertPathPlaceholder'), trigger: 'blur' }
]
}
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
@@ -143,45 +140,39 @@ const emit = defineEmits(['complete'])
* 确认 * 确认
* @param formEl * @param formEl
*/ */
const confirm = async (formEl: FormInstance | undefined) => { const confirm = async () => { if (loading.value) return; formApi.submit() }
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
emit('complete', formData)
showDialog.value = false
}
})
}
const cancel = () => { const cancel = () => {
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (initData.value) { if (initData.value) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (initData.value[key] != undefined) formData[key] = initData.value[key] if (initData.value[key] != undefined) formModel[key] = initData.value[key]
}) })
formData.channel = initData.value.redio_key.split('_')[0] formModel.channel = initData.value.redio_key.split('_')[0]
formData.status = Number(formData.status) formModel.status = Number(formModel.status)
} }
emit('complete', formData) emit('complete', formModel)
showDialog.value = false modalApi.close()
} }
const setFormData = async (data: any = null) => { const setFormData = async (data: any = null) => {
initData.value = cloneDeep(data) initData.value = cloneDeep(data)
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (data) { if (data) {
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key] if (data[key] != undefined) formModel[key] = data[key]
}) })
formData.channel = data.redio_key.split('_')[0] formModel.channel = data.redio_key.split('_')[0]
formData.status = Number(formData.status) formModel.status = Number(formModel.status)
} }
formApi.setModel({ ...formModel })
loading.value = false loading.value = false
modalApi.open()
} }
const enableVerify = () => { const enableVerify = () => {
let verify = true let verify = true
if (Test.empty(formData.config.mch_id) || Test.empty(formData.config.mch_secret_key) || Test.empty(formData.config.mch_secret_cert) || Test.empty(formData.config.mch_public_cert_path)) verify = false if (Test.empty(formModel.config.mch_id) || Test.empty(formModel.config.mch_secret_key) || Test.empty(formModel.config.mch_secret_cert) || Test.empty(formModel.config.mch_public_cert_path)) verify = false
return verify return verify
} }
@@ -210,7 +201,6 @@ watch(copied, () => {
}) })
defineExpose({ defineExpose({
showDialog,
setFormData, setFormData,
enableVerify enableVerify
}) })

View File

@@ -1,6 +1,6 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('aliSms')" width="580px" :destroy-on-close="true"> <Modal :class="'w-[580px]'" :title="t('aliSms')">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('isUse')"> <el-form-item :label="t('isUse')">
<el-radio-group v-model="formData.is_use"> <el-radio-group v-model="formData.is_use">
<el-radio :label="1">{{ t('startUsing') }}</el-radio> <el-radio :label="1">{{ t('startUsing') }}</el-radio>
@@ -20,24 +20,23 @@
<el-input v-model.trim="formData.secret_key" :placeholder="t('aliSecretKeyPlaceholder')" class="input-width" clearable /> <el-input v-model.trim="formData.secret_key" :placeholder="t('aliSecretKeyPlaceholder')" class="input-width" clearable />
</el-form-item> </el-form-item>
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getSmsInfo, editSms } from '@/app/api/notice' import { getSmsInfo, editSms } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -50,23 +49,21 @@ const initialFormData = {
secret_key: '', secret_key: '',
is_use: '' is_use: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const [BaseForm, formApi] = useVbenForm({
const formRef = ref<FormInstance>() commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
// 表单验证规则 loading.value = true
const formRules = computed(() => { editSms(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
return { },
sign: [ layout: 'horizontal',
{ required: true, message: t('aliSignPlaceholder'), trigger: 'blur' } schema: [
], { component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
app_key: [ { component: 'Input', fieldName: 'sign', label: t('aliSign'), rules: [{ required: true }] },
{ required: true, message: t('aliAppKeyPlaceholder'), trigger: 'blur' } { component: 'Input', fieldName: 'app_key', label: t('aliAppKey'), rules: [{ required: true }] },
], { component: 'Input', fieldName: 'secret_key', label: t('aliSecretKey'), rules: [{ required: true }] }
secret_key: [ ],
{ required: true, message: t('aliSecretKeyPlaceholder'), trigger: 'blur' } wrapperClass: 'grid-cols-1'
]
}
}) })
const emit = defineEmits(['complete']) const emit = defineEmits(['complete'])
@@ -75,44 +72,26 @@ const emit = defineEmits(['complete'])
* 确认 * 确认
* @param formEl * @param formEl
*/ */
const confirm = async (formEl: FormInstance | undefined) => { const confirm = async () => { if (loading.value) return; formApi.submit() }
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editSms(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
// showDialog.value = false
})
}
})
}
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (row) { if (row) {
const data = await (await getSmsInfo(row.sms_type)).data const data = await (await getSmsInfo(row.sms_type)).data
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key] if (data[key] != undefined) formModel[key] = data[key]
if (data.params[key] != undefined) formData[key] = data.params[key].value if (data.params && data.params[key] != undefined) formModel[key] = data.params[key].value
}) })
} }
loading.value = false formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
} }
defineExpose({ const cancel = () => { modalApi.close() }
showDialog,
setFormData defineExpose({ setFormData })
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,51 +1,38 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('messageInfo')" width="550px" :destroy-on-close="true"> <Modal :class="'w-[550px]'" :title="t('messageInfo')">
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <div class="page-form" v-loading="loading">
<el-form-item :label="t('messageKey')">
<el-form-item :label="t('messageKey')"> <div class="input-width"> {{ formModel.name }} </div>
<div class="input-width"> {{ formData.name }} </div> </el-form-item>
</el-form-item> <el-form-item :label="t('smsType')">
<div class="input-width"> {{ formModel.sms_type_name }} </div>
<el-form-item :label="t('smsType')"> </el-form-item>
<div class="input-width"> {{ formData.sms_type_name }} </div> <el-form-item :label="t('receiver')">
</el-form-item> <div class="input-width"> {{ formModel.mobile }} </div>
</el-form-item>
<!-- <el-form-item :label="t('messageData')"> <el-form-item :label="t('createTime')">
<div class="input-width"> {{ formData.message_data }} </div> <div class="input-width"> {{ formModel.create_time }} </div>
</el-form-item> --> </el-form-item>
<el-form-item :label="t('发送结果')">
<!-- <el-form-item :label="t('nickname')"> <div class="input-width" v-if="formModel.status == 'sending'"> 发送失败 </div>
<div class="input-width"> {{ formData.nickname }} </div> <div class="input-width" v-if="formModel.status == 'success'"> 发送成功 </div>
</el-form-item> --> <div class="input-width" v-if="formModel.status == 'fail'"> {{ formModel.result }} </div>
</el-form-item>
<el-form-item :label="t('receiver')"> </div>
<div class="input-width"> {{ formData.mobile }} </div> <template #footer>
</el-form-item> <span class="dialog-footer">
<el-button type="primary" @click="close()">{{ t('confirm') }}</el-button>
<el-form-item :label="t('createTime')"> </span>
<div class="input-width"> {{ formData.create_time }} </div> </template>
</el-form-item> </Modal>
<el-form-item :label="t('发送结果')">
<div class="input-width" v-if="formData.status == 'sending'"> 发送失败 </div>
<div class="input-width" v-if="formData.status == 'success'"> 发送成功 </div>
<div class="input-width" v-if="formData.status == 'fail'"> {{ formData.result }} </div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="showDialog = false">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import { useVbenModal } from '@vben/common-ui'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/** /**
@@ -63,34 +50,21 @@ const initialFormData = {
status:'', status:'',
result:'' result:''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
// 表单验证规则
const formRules = computed(() => {
return {
}
})
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (row) {
if (row) { Object.keys(formModel).forEach((key: string) => { if (row[key] != undefined) formModel[key] = row[key] })
Object.keys(formData).forEach((key: string) => { }
if (row[key] != undefined) formData[key] = row[key] loading.value = false
}) modalApi.open()
}
loading.value = false
} }
defineExpose({ const close = () => { modalApi.close() }
showDialog,
setFormData defineExpose({ setFormData })
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -1,52 +1,25 @@
<template> <template>
<el-dialog v-model="showDialog" :title="t('tencentSms')" width="580px" :destroy-on-close="true"> <Modal :class="'w-[580px]'" :title="t('tencentSms')">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <BaseForm />
<el-form-item :label="t('isUse')">
<el-radio-group v-model="formData.is_use">
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('tencentSign')" prop="sign">
<el-input v-model.trim="formData.sign" :placeholder="t('tencentSignPlaceholder')" class="input-width" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('tencentAppId')" prop="app_id">
<el-input v-model.trim="formData.app_id" :placeholder="t('tencentAppIdPlaceholder')" class="input-width" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('tencentSecretId')" prop="secret_id">
<el-input v-model.trim="formData.secret_id" :placeholder="t('tencentSecretIdPlaceholder')" class="input-width" clearable />
</el-form-item>
<el-form-item :label="t('tencentSecretKey')" prop="secret_key">
<el-input v-model.trim="formData.secret_key" :placeholder="t('tencentSecretKeyPlaceholder')" class="input-width" clearable />
</el-form-item>
</el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <el-button @click="cancel">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button> <el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
</span> </span>
</template> </template>
</el-dialog> </Modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getSmsInfo, editSms } from '@/app/api/notice' import { getSmsInfo, editSms } from '@/app/api/notice'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
const showDialog = ref(false) const [Modal, modalApi] = useVbenModal()
const loading = ref(true) const loading = ref(true)
/**
* 表单数据
*/
const initialFormData = { const initialFormData = {
sms_type: '', sms_type: '',
sign: '', sign: '',
@@ -56,72 +29,44 @@ const initialFormData = {
app_id: '', app_id: '',
secret_id: '' secret_id: ''
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formModel: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>() const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
// 表单验证规则 handleSubmit: async (values) => {
const formRules = computed(() => { loading.value = true
return { editSms(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
sign: [ },
{ required: true, message: t('tencentSignPlaceholder'), trigger: 'blur' } layout: 'horizontal',
], schema: [
app_id: [ { component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
{ required: true, message: t('tencentAppIdPlaceholder'), trigger: 'blur' } { component: 'Input', fieldName: 'sign', label: t('tencentSign'), rules: [{ required: true }] },
], { component: 'Input', fieldName: 'app_id', label: t('tencentAppId'), rules: [{ required: true }] },
secret_id: [ { component: 'Input', fieldName: 'secret_id', label: t('tencentSecretId'), rules: [{ required: true }] },
{ required: true, message: t('tencentSecretIdPlaceholder'), trigger: 'blur' } { component: 'Input', fieldName: 'secret_key', label: t('tencentSecretKey'), rules: [{ required: true }] }
], ],
secret_key: [ wrapperClass: 'grid-cols-1'
{ required: true, message: t('tencentSecretKeyPlaceholder'), trigger: 'blur' }
]
}
}) })
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editSms(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
// showDialog.value = false
})
}
})
}
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
Object.assign(formData, initialFormData) Object.assign(formModel, initialFormData)
if (row) { if (row) {
const data = await (await getSmsInfo(row.sms_type)).data const data = await (await getSmsInfo(row.sms_type)).data
Object.keys(formData).forEach((key: string) => { Object.keys(formModel).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key] if (data[key] != undefined) formModel[key] = data[key]
if (data.params[key] != undefined) formData[key] = data.params[key].value if (data.params && data.params[key] != undefined) formModel[key] = data.params[key].value
}) })
} }
loading.value = false formApi.setModel({ ...formModel })
loading.value = false
modalApi.open()
} }
defineExpose({ const emit = defineEmits(['complete'])
showDialog, const cancel = () => { modalApi.close() }
setFormData
}) defineExpose({ setFormData })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -138,6 +138,12 @@
</el-form> </el-form>
</div> </div>
<ModalPassword :class="'w-[400px]'" :title="'请保存好新密码'">
<div class="p-2"> 新密码为{{ newPassword }} </div>
<template #footer>
<el-button type="primary" @click="passwordConfirm">{{ t('confirm') }}</el-button>
</template>
</ModalPassword>
</el-card> </el-card>
</template> </template>
@@ -145,6 +151,7 @@
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from 'vue'
import { loginAccount, getSmsCaptcha, getSmsSend, resetPassword, registerAccount, getSmsSignConfig } from '@/app/api/notice' import { loginAccount, getSmsCaptcha, getSmsSend, resetPassword, registerAccount, getSmsSignConfig } from '@/app/api/notice'
import { t } from '@/lang' import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({ const props = defineProps({
info: { info: {
@@ -501,22 +508,17 @@ const reset = async () => {
mobile: changeFormData.value.mobile mobile: changeFormData.value.mobile
} }
resetPassword(changeFormData.value.username, { ...params }).then((res) => { resetPassword(changeFormData.value.username, { ...params }).then((res) => {
const newPassword = res.data.password newPassword.value = res.data.password
ElMessageBox.confirm(`新密码为:${newPassword}`, '请保存好新密码', { modalPasswordApi.open()
confirmButtonText: '确定',
showCancelButton: false
}).then(() => {
type.value = 'login'
emit('complete')
}).catch(() => {
type.value = 'login'
emit('complete')
})
}) })
} }
}) })
} }
const [ModalPassword, modalPasswordApi] = useVbenModal()
const newPassword = ref('')
const passwordConfirm = () => { modalPasswordApi.close(); type.value = 'login'; emit('complete') }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -26,40 +26,42 @@
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit" <el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total" @size-change="loadRankList()" @current-change="loadRankList" /> layout="total, sizes, prev, pager, next, jumper" :total="tableData.total" @size-change="loadRankList()" @current-change="loadRankList" />
</div> </div>
<el-dialog v-model="visibleDetail" :title="t('模版详情')" width="600px" destroy-on-close > <ModalDetail :class="'w-[600px]'" :title="t('模版详情')">
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="loading"> <div v-loading="loading">
<el-form-item :label="t('订单编号')" prop="template_id"> <el-form label-width="100px" ref="formRef" class="page-form">
<div>{{ detail.order_no }}</div> <el-form-item :label="t('订单编号')" prop="template_id">
</el-form-item> <div>{{ detail.order_no }}</div>
<el-form-item :label="t('用户名称')" prop="template_id"> </el-form-item>
<div>{{ detail.username }}</div> <el-form-item :label="t('用户名称')" prop="template_id">
</el-form-item> <div>{{ detail.username }}</div>
<el-form-item :label="t('套餐名称')" prop="template_id"> </el-form-item>
<div>{{ detail.package_name }}</div> <el-form-item :label="t('套餐名称')" prop="template_id">
</el-form-item> <div>{{ detail.package_name }}</div>
<el-form-item :label="t('订单状态')" prop="title"> </el-form-item>
<div >{{ detail.order_status_name }}</div> <el-form-item :label="t('订单状态')" prop="title">
</el-form-item> <div>{{ detail.order_status_name }}</div>
<el-form-item :label="t('短信条数')" prop="title"> </el-form-item>
<div >{{ detail.sms_num }}</div> <el-form-item :label="t('短信条数')" prop="title">
</el-form-item> <div>{{ detail.sms_num }}</div>
<el-form-item :label="t('订单金额')" prop="title"> </el-form-item>
<div >{{ detail.order_money }}</div> <el-form-item :label="t('订单金额')" prop="title">
</el-form-item> <div>{{ detail.order_money }}</div>
<el-form-item :label="t('付款金额')" prop="title"> </el-form-item>
<div >{{ detail.pay_money }}</div> <el-form-item :label="t('付款金额')" prop="title">
</el-form-item> <div>{{ detail.pay_money }}</div>
<el-form-item :label="t('创建时间')" prop="title"> </el-form-item>
<div >{{ detail.create_time }}</div> <el-form-item :label="t('创建时间')" prop="title">
</el-form-item> <div>{{ detail.create_time }}</div>
</el-form> </el-form-item>
</el-form>
</div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="visibleDetail = false">{{ t("cancel") }}</el-button> <el-button @click="detailCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="visibleDetail = false">{{ t("confirm") }}</el-button> <el-button type="primary" @click="detailConfirm">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalDetail>
</div> </div>
</template> </template>
@@ -67,6 +69,7 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { getSmsOrdersList, getOrderInfo } from '@/app/api/notice' import { getSmsOrdersList, getOrderInfo } from '@/app/api/notice'
import { t } from '@/lang' import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({ const props = defineProps({
username: { username: {
@@ -106,18 +109,19 @@ const loadRankList = () => {
} }
// 详情 // 详情
const [ModalDetail, modalDetailApi] = useVbenModal()
const detail = ref({}) const detail = ref({})
const visibleDetail = ref(false)
const loading = ref(false) const loading = ref(false)
const detailEvent = (row:any) => { const detailEvent = (row:any) => {
loading.value = true loading.value = true
visibleDetail.value = true modalDetailApi.open()
getOrderInfo(props.username, { out_trade_no: row.out_trade_no }).then(res => { getOrderInfo(props.username, { out_trade_no: row.out_trade_no }).then(res => {
detail.value = res.data detail.value = res.data
loading.value = false loading.value = false
}) })
visibleDetail.value = true
} }
const detailCancel = () => { modalDetailApi.close() }
const detailConfirm = () => { modalDetailApi.close() }
onMounted(() => { onMounted(() => {
if (props.username) { if (props.username) {
loadRankList() loadRankList()

View File

@@ -81,71 +81,32 @@
<el-button @click="visible = false">{{ t("cancel") }}</el-button> <el-button @click="visible = false">{{ t("cancel") }}</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="visibleAdd" :title="t('添加签名')" width="800px" destroy-on-close :close-on-click-modal="false"> <Modal :class="'w-[800px]'" :title="t('添加签名')">
<el-form label-width="150px" :model="formData" ref="formRef" :rules="formRules" class="page-form ml-[20px]"> <BaseForm />
<el-form-item :label="t('短信签名')" prop="signature"> <div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由包裹例如test</div>
<el-input v-model="formData.signature" placeholder="请输入短信签名" class="input-width" maxlength="20" show-word-limit clearable /> <div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符不能使用空格和特殊符号 - + = * & % # @ ~;</div>
</el-form-item> <el-form-item :label="t('上传图片')">
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由包裹例如test</div> <upload-image v-model="formData.imgUrl" :limit="1" />
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符不能使用空格和特殊符号 - + = * & % # @ ~;</div> </el-form-item>
<el-form-item :label="t('短信示例内容')" prop="contentExample"> <div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为商标APP小程序事业单位简称或企业名称简称时需必填此字段</div>
<el-input v-model="formData.contentExample" placeholder="请输入短信示例内容" clearable maxlength="50" show-word-limit class="input-width" /> <div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为事业单位全称或企业名称全称时选填此字段</div>
</el-form-item>
<el-form-item :label="t('企业名称')" prop="companyName">
<el-input v-model="formData.companyName" placeholder="请输入企业名称" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('社会统一信用代码')" prop="creditCode">
<el-input v-model="formData.creditCode" placeholder="请输入社会统一信用代码" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('法人姓名')" prop="legalPerson">
<el-input v-model="formData.legalPerson" placeholder="请输入法人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人姓名')" prop="principalName">
<el-input v-model="formData.principalName" placeholder="请输入经办人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人手机号')" prop="principalMobile">
<el-input v-model="formData.principalMobile" placeholder="请输入经办人手机号" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人身份证')" prop="principalIdCard">
<el-input v-model="formData.principalIdCard" placeholder="请输入经办人身份证" clearable maxlength="18" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('签名来源')">
<el-radio-group v-model="formData.signSource" >
<el-radio v-for="item in signConfig.signSourceList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('签名类型')">
<el-radio-group v-model="formData.signType">
<el-radio v-for="item in signConfig.signTypeList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('上传图片')" prop="imgUrl">
<upload-image v-model="formData.imgUrl" :limit="1" />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为商标APP小程序事业单位简称或企业名称简称时需必填此字段</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为事业单位全称或企业名称全称时选填此字段</div>
<el-form-item :label="t('是否默认')">
<el-radio-group v-model="formData.defaultSign" >
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer> <template #footer>
<el-button @click="visibleAdd = false">{{ t("cancel") }}</el-button> <el-button @click="modalApi.close()">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="onSave()">{{ t("confirm") }}</el-button> <el-button type="primary" @click="submitVben()">{{ t("confirm") }}</el-button>
</template> </template>
</el-dialog> </Modal>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from 'vue'
import { useVbenModal } from '@vben/common-ui'
import { useVbenForm } from '@/_env/adapter/form'
import { getSignList, addSign, getSmsSignConfig, deleteSign } from '@/app/api/notice' import { getSignList, addSign, getSmsSignConfig, deleteSign } from '@/app/api/notice'
import { t } from '@/lang' import { t } from '@/lang'
const visible = ref(false) const visible = ref(false)
const visibleAdd = ref(false) const [Modal, modalApi] = useVbenModal()
const emit = defineEmits(['select']) const emit = defineEmits(['select'])
const props = defineProps({ const props = defineProps({
username: { username: {
@@ -185,6 +146,28 @@ const getSmsSignConfigFn = () => {
getSmsSignConfigFn() getSmsSignConfigFn()
const formRef = ref() const formRef = ref()
const [BaseForm, formApi] = useVbenForm({
commonConfig: { componentProps: { class: 'w-full' } },
handleSubmit: async (values) => {
const merged = { ...values, imgUrl: formData.imgUrl, defaultSign: formData.defaultSign, signSource: formData.signSource, signType: formData.signType }
addSign(props.username, merged).then(() => { setTimeout(() => { modalApi.close(); loadSignList() }, 500) })
},
layout: 'horizontal',
schema: [
{ component: 'Input', fieldName: 'signature', label: t('短信签名'), rules: [{ required: true }, { validator: (v) => /^[^]*$/.test(v) ? true : '短信签名必须被【】包裹' }] },
{ component: 'Input', fieldName: 'contentExample', label: t('短信示例内容'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'companyName', label: t('企业名称'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'creditCode', label: t('社会统一信用代码'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'legalPerson', label: t('法人姓名'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'principalName', label: t('经办人姓名'), rules: [{ required: true }] },
{ component: 'Input', fieldName: 'principalMobile', label: t('经办人手机号'), rules: [{ required: true }, { validator: (v) => /^1[3-9]\d{9}$/.test(v) ? true : t('请输入正确的手机号码') }] },
{ component: 'Input', fieldName: 'principalIdCard', label: t('经办人身份证'), rules: [{ required: true }, { validator: (v) => /^[1-9]\d{5}(19|20)\d{2}((0\d)|(1[0-2]))(([0-2]\d)|3[0-1])\d{3}([0-9Xx])$/.test(v) ? true : t('请输入正确的身份证号码') }] },
{ component: 'RadioGroup', fieldName: 'signSource', label: t('签名来源'), componentProps: { options: () => signConfig.signSourceList.map((i:any) => ({ label: i.name, value: i.type })) } },
{ component: 'RadioGroup', fieldName: 'signType', label: t('签名类型'), componentProps: { options: () => signConfig.signTypeList.map((i:any) => ({ label: i.name, value: i.type })) } },
{ component: 'RadioGroup', fieldName: 'defaultSign', label: t('是否默认'), componentProps: { options: [ { label: '是', value: 1 }, { label: '否', value: 0 } ] } }
],
wrapperClass: 'grid-cols-1'
})
const formRules = computed(() => { const formRules = computed(() => {
return { return {
signature: [ signature: [
@@ -273,18 +256,7 @@ const phoneVerify = (rule: any, value: any, callback: any) => {
} }
} }
const onSave = async () => { const submitVben = () => { formApi.submit() }
await formRef.value?.validate(async (valid) => {
if (valid) {
addSign(props.username, formData).then((res) => {
setTimeout(() => {
visibleAdd.value = false
loadSignList()
}, 500)
})
}
})
}
// 表单内容 // 表单内容
const tableData = reactive({ const tableData = reactive({
@@ -321,7 +293,8 @@ const addEvent = () => {
Object.assign(formData, initialFormData) Object.assign(formData, initialFormData)
formData.signSource = signConfig.signSourceList[0].type formData.signSource = signConfig.signSourceList[0].type
formData.signType = signConfig.signTypeList[0].type formData.signType = signConfig.signTypeList[0].type
visibleAdd.value = true formApi.setModel({ ...formData })
modalApi.open()
} }
const deleteTemplate = (row:any) => { const deleteTemplate = (row:any) => {

View File

@@ -54,67 +54,66 @@
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit" <el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total"/> layout="total, sizes, prev, pager, next, jumper" :total="tableData.total"/>
</div> </div>
<el-dialog v-model="visibleDetail" :title="t('模版详情')" width="600px" destroy-on-close > <ModalDetail :class="'w-[600px]'" :title="t('模版详情')">
<el-form label-width="100px" ref="formRef" class="page-form"> <el-form label-width="100px" ref="formRef" class="page-form">
<el-form-item :label="t('短信类型')" prop="template_id"> <el-form-item :label="t('短信类型')" prop="template_id">
<div>{{ detail.sms_type }}</div> <div>{{ detail?.sms_type }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('模版名称')" prop="template_id"> <el-form-item :label="t('模版名称')" prop="template_id">
<div>{{ detail.name }}</div> <div>{{ detail?.name }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('模版类型')" prop="title"> <el-form-item :label="t('模版类型')" prop="title">
<div >{{ detail.title }}</div> <div>{{ detail?.title }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('短信内容')" prop="title" v-if="detail.sms"> <el-form-item :label="t('短信内容')" prop="title" v-if="detail?.sms">
<div >{{ detail.sms?.content }}</div> <div>{{ detail?.sms?.content }}</div>
</el-form-item> </el-form-item>
<el-form-item :label="t('审核状态')" prop="title"> <el-form-item :label="t('审核状态')" prop="title">
<div >{{ detail.audit_info.audit_status_name }}</div> <div>{{ detail?.audit_info?.audit_status_name }}</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<!-- <el-button @click="visibleDetail = false">{{ t("cancel") }}</el-button> --> <el-button type="primary" @click="detailConfirm">{{ t('confirm') }}</el-button>
<el-button type="primary" @click="visibleDetail = false">{{ t("confirm") }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalDetail>
<el-dialog v-model="visibleReport" :title="t('模版报备')" width="820px" destroy-on-close > <ModalReport :class="'w-[820px]'" :title="t('模版报备')">
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="reportLoading"> <div v-loading="reportLoading">
<el-form-item :label="t('模版名称')" prop="template_id"> <el-form label-width="100px" ref="formRef" class="page-form">
<div class="input-width">{{ detail.name }}</div> <el-form-item :label="t('模版名称')" prop="template_id">
</el-form-item> <div class="input-width">{{ detail?.name }}</div>
<el-form-item :label="t('模版类型')" prop="title"> </el-form-item>
<el-radio-group v-model="reportData.template_type"> <el-form-item :label="t('模版类型')" prop="title">
<el-radio v-for="[key, value] in Object.entries(template_type_list)" :key="key" :label="Number(key)">{{ value }}</el-radio> <el-radio-group v-model="reportData.template_type">
</el-radio-group> <el-radio v-for="[key, value] in Object.entries(template_type_list)" :key="key" :label="Number(key)">{{ value }}</el-radio>
</el-form-item> </el-radio-group>
<div class="ml-[100px] mb-[10px] mt-[-10px] text-[12px] text-[#999] leading-[20px]"> </el-form-item>
<div>验证码仅支持验证码类型变量</div> <div class="ml-[100px] mb-[10px] mt-[-10px] text-[12px] text-[#999] leading-[20px]">
<div>行业通知支持验证码类型变量</div> <div>验证码支持验证码类型变量</div>
<div>营销推广不支持变量</div> <div>行业通知不支持验证码类型变量</div>
</div> <div>营销推广不支持变量</div>
<el-form-item :label="t('变量类型')" prop="params_json" v-if="detail.variable && Object.keys(detail.variable).length > 0">
<div v-for="(label, key) in detail.variable" :key="key" class="mb-2 flex items-center">
<div class="flex flex-1 items-center">
<div class="w-32 mr-1 ">{{ label }}</div>
<el-select v-model="reportData.params_json[key]" placeholder="请选择类型" class="flex-1" filterable clearable :disabled="isMarketingWithVariable">
<el-option v-for="item in filteredParamTypes" :key="item.type" :label="item.name + '' + item.desc + ''" :value="item.type"/>
</el-select>
</div>
</div> </div>
</el-form-item> <el-form-item :label="t('变量类型')" prop="params_json" v-if="detail?.variable && Object.keys(detail.variable).length > 0">
<div v-for="(label, key) in detail.variable" :key="key" class="mb-2 flex items-center">
</el-form> <div class="flex flex-1 items-center">
<div class="w-32 mr-1 ">{{ label }}</div>
<el-select v-model="reportData.params_json[key]" placeholder="请选择类型" class="flex-1" filterable clearable :disabled="isMarketingWithVariable">
<el-option v-for="item in filteredParamTypes" :key="item.type" :label="item.name + '' + item.desc + ''" :value="item.type"/>
</el-select>
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="visibleReport = false">{{ t("cancel") }}</el-button> <el-button @click="reportCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="reportTemplateFn()" :disabled="isMarketingWithVariable">{{ t("confirm") }}</el-button> <el-button type="primary" @click="reportConfirm" :disabled="isMarketingWithVariable">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalReport>
<el-dialog v-model="visibleAsync" :title="t('同步模版状态')" width="800px" destroy-on-close > <ModalAsync :class="'w-[800px]'" :title="t('同步模版状态')">
<el-alert type="warning" :closable="false" class="!mb-[10px]"> <el-alert type="warning" :closable="false" class="!mb-[10px]">
<template #default> <template #default>
以下模版名称重复请先调整模版名称后重新同步模版 以下模版名称重复请先调整模版名称后重新同步模版
@@ -125,20 +124,20 @@
<el-table :data="repeatListArray" border style="width: 100%;"> <el-table :data="repeatListArray" border style="width: 100%;">
<el-table-column label="模版名称" prop="name" /> <el-table-column label="模版名称" prop="name" />
<el-table-column label="插件名称"> <el-table-column label="插件名称">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-for="item in row.platforms" :key="item" class="mr-1 mb-1">{{ item }}</el-tag> <el-tag v-for="item in row.platforms" :key="item" class="mr-1 mb-1">{{ item }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="visibleAsync = false">{{ t("cancel") }}</el-button> <el-button @click="asyncCancel">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="visibleAsync = false">{{ t("confirm") }}</el-button> <el-button type="primary" @click="asyncConfirm">{{ t('confirm') }}</el-button>
</span> </span>
</template> </template>
</el-dialog> </ModalAsync>
</div> </div>
</template> </template>
@@ -146,6 +145,7 @@
import { ref, computed, reactive, onMounted, watch } from 'vue' import { ref, computed, reactive, onMounted, watch } from 'vue'
import { getTemplateList, getTemplateReportConfig, reportTemplate, templateSync, getreportTemplateInfo, clearTemplate } from '@/app/api/notice' import { getTemplateList, getTemplateReportConfig, reportTemplate, templateSync, getreportTemplateInfo, clearTemplate } from '@/app/api/notice'
import { t } from '@/lang' import { t } from '@/lang'
import { useVbenModal } from '@vben/common-ui'
const props = defineProps({ const props = defineProps({
username: { username: {
@@ -222,13 +222,15 @@ onMounted(() => {
} }
}) })
const visibleAsync = ref(false) const [ModalDetail, modalDetailApi] = useVbenModal()
const [ModalReport, modalReportApi] = useVbenModal()
const [ModalAsync, modalAsyncApi] = useVbenModal()
const repeatList = ref({}) const repeatList = ref({})
const syncEvent = () => { const syncEvent = () => {
templateSync('niuyun', props.username).then((res) => { templateSync('niuyun', props.username).then((res) => {
repeatList.value = res.data.repeat_list repeatList.value = res.data.repeat_list
if (repeatList.value && Object.keys(repeatList.value).length > 0) { if (repeatList.value && Object.keys(repeatList.value).length > 0) {
visibleAsync.value = true modalAsyncApi.open()
} else { } else {
loadSmsTemplateList() loadSmsTemplateList()
} }
@@ -244,10 +246,9 @@ const repeatListArray = computed(() => {
// 详情 // 详情
const detail = ref(null) const detail = ref(null)
const visibleDetail = ref(false)
const editEvent = (row:any) => { const editEvent = (row:any) => {
visibleDetail.value = true
detail.value = row detail.value = row
modalDetailApi.open()
} }
// 清除报备 // 清除报备
@@ -267,7 +268,6 @@ const clearEvent = (row:any) => {
const template_params_type_list = ref({}) const template_params_type_list = ref({})
const template_type_list = ref({}) const template_type_list = ref({})
const template_status_list = ref({}) const template_status_list = ref({})
const visibleReport = ref(false)
const reportData = ref({ const reportData = ref({
template_type: 1, template_type: 1,
template_key: '', template_key: '',
@@ -307,9 +307,9 @@ const reportEvent = (row:any) => {
if (!signature) { if (!signature) {
ElMessage.error('请先配置签名') ElMessage.error('请先配置签名')
} else { } else {
detail.value = row
modalReportApi.open()
if (row.template_id) { if (row.template_id) {
visibleReport.value = true
detail.value = row
getreportTemplateInfo('niuyun', props.username, { template_key: row.key }).then((res) => { getreportTemplateInfo('niuyun', props.username, { template_key: row.key }).then((res) => {
const paramJson = res.data?.param_json ?? {} const paramJson = res.data?.param_json ?? {}
reportData.value.template_key = res.data.template_key reportData.value.template_key = res.data.template_key
@@ -323,9 +323,7 @@ const reportEvent = (row:any) => {
reportLoading.value = false reportLoading.value = false
}) })
} else { } else {
visibleReport.value = true
reportLoading.value = false reportLoading.value = false
detail.value = row
reportData.value.template_type = 1 reportData.value.template_type = 1
reportData.value.template_key = detail.value.key reportData.value.template_key = detail.value.key
reportData.value.params_json = {} reportData.value.params_json = {}
@@ -350,10 +348,16 @@ const reportTemplateFn = () => {
reportData.value.template_id = Number(detail.value.template_id) reportData.value.template_id = Number(detail.value.template_id)
} }
reportTemplate(detail.value.sms_type, props.username, reportData.value).then((res) => { reportTemplate(detail.value.sms_type, props.username, reportData.value).then((res) => {
visibleReport.value = false modalReportApi.close()
loadSmsTemplateList() loadSmsTemplateList()
}) })
} }
const detailConfirm = () => { modalDetailApi.close() }
const reportCancel = () => { modalReportApi.close() }
const reportConfirm = () => { reportTemplateFn() }
const asyncCancel = () => { modalAsyncApi.close() }
const asyncConfirm = () => { modalAsyncApi.close() }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

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