chore: push latest changes
This commit is contained in:
182
.trae/documents/aiqiyi_video_app_prd.md
Normal file
182
.trae/documents/aiqiyi_video_app_prd.md
Normal 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 用户体验
|
||||
|
||||
- **启动速度**:分包加载,预加载关键资源
|
||||
- **播放流畅度**:多码率适配,缓冲策略优化
|
||||
- **交互响应**:防抖节流,异步处理,骨架屏
|
||||
- **离线体验**:内容缓存,离线阅读,断网提示
|
||||
- **多端同步**:观看进度,收藏列表,阅读书签
|
||||
549
.trae/documents/aiqiyi_video_app_technical_architecture.md
Normal file
549
.trae/documents/aiqiyi_video_app_technical_architecture.md
Normal 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
|
||||
|
||||
* **预加载策略**:智能预加载、按需加载、优先级加载
|
||||
|
||||
* **多码率适配**:根据网络自动选择清晰度
|
||||
|
||||
* **断点续传**:支持大文件断点续传,提升用户体验
|
||||
|
||||
@@ -93,6 +93,41 @@ export const getCustomMenuApi = () => {
|
||||
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: {
|
||||
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');
|
||||
};
|
||||
|
||||
// 导出用户
|
||||
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) => {
|
||||
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 });
|
||||
};
|
||||
|
||||
// 获取素材详情
|
||||
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');
|
||||
};
|
||||
|
||||
// 上传素材
|
||||
export const uploadMaterialApi = (data: FormData) => {
|
||||
export const uploadWechatMaterial = (data: FormData) => {
|
||||
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) => {
|
||||
return request.delete(`/adminapi/wechat/material/${id}`);
|
||||
|
||||
@@ -18,6 +18,45 @@
|
||||
"appList": "应用列表",
|
||||
"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": {
|
||||
"weapp": {
|
||||
"title": "微信小程序",
|
||||
@@ -56,4 +95,4 @@
|
||||
"list": "存储配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
218
admin-vben/apps/web-antd/src/router/routes/modules/admin.ts
Normal file
218
admin-vben/apps/web-antd/src/router/routes/modules/admin.ts
Normal 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;
|
||||
135
admin-vben/apps/web-antd/src/views/app/list/data.ts
Normal file
135
admin-vben/apps/web-antd/src/views/app/list/data.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
269
admin-vben/apps/web-antd/src/views/app/list/list.vue
Normal file
269
admin-vben/apps/web-antd/src/views/app/list/list.vue
Normal 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>
|
||||
97
admin-vben/apps/web-antd/src/views/app/list/modules/form.vue
Normal file
97
admin-vben/apps/web-antd/src/views/app/list/modules/form.vue
Normal 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>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
media_id: string;
|
||||
type: 'image' | 'voice' | 'video' | 'news';
|
||||
type: 'image' | 'voice' | 'video' | 'news' | 'thumb';
|
||||
title?: string;
|
||||
introduction?: string;
|
||||
url: string;
|
||||
thumb_url?: string;
|
||||
content?: string;
|
||||
digest?: string;
|
||||
show_cover_pic: 0 | 1;
|
||||
author?: string;
|
||||
content_source_url?: string;
|
||||
local_url?: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
news_item?: NewsItem[];
|
||||
status: 0 | 1;
|
||||
create_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 {
|
||||
id?: number;
|
||||
type: 'image' | 'voice' | 'video' | 'news';
|
||||
type: string;
|
||||
title?: string;
|
||||
introduction?: string;
|
||||
file?: File;
|
||||
news_item?: NewsItem[];
|
||||
status: 0 | 1;
|
||||
url?: string;
|
||||
thumb_url?: string;
|
||||
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: 'voice' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '图文', value: 'news' },
|
||||
{ label: '缩略图', value: 'thumb' },
|
||||
];
|
||||
|
||||
export const materialTypeMap = {
|
||||
image: '图片',
|
||||
voice: '语音',
|
||||
video: '视频',
|
||||
news: '图文',
|
||||
};
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
export const showCoverOptions = [
|
||||
{ label: '不显示', value: 0 },
|
||||
{ label: '显示', value: 1 },
|
||||
];
|
||||
|
||||
export const statusMap = {
|
||||
1: '正常',
|
||||
0: '禁用',
|
||||
};
|
||||
|
||||
export const querySchema = [
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '素材类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: materialTypeOptions,
|
||||
placeholder: '请选择素材类型',
|
||||
export const gridOptions: VxeGridProps<WechatMaterial> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'media_id', title: 'MediaID', width: 180 },
|
||||
{ field: 'type', title: '类型', width: 100, formatter: ({ cellValue }) => {
|
||||
const option = typeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'title', title: '标题', minWidth: 150 },
|
||||
{ field: 'url', title: 'URL', minWidth: 200, showOverflow: true },
|
||||
{ field: 'thumb_url', title: '缩略图', width: 120, formatter: ({ cellValue }) => {
|
||||
return cellValue ? `<img src="${cellValue}" style="width: 60px; height: 60px; object-fit: cover;" />` : '';
|
||||
} },
|
||||
{ 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],
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '标题',
|
||||
component: 'Input',
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// 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' },
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,235 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-4">
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:form-options="formOptions"
|
||||
: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">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
上传素材
|
||||
</VbenButton>
|
||||
<VbenButton type="success" @click="handleSync">
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
<VbenButton type="default" @click="handleSync">
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
同步素材
|
||||
</VbenButton>
|
||||
</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>
|
||||
|
||||
<MaterialUploadModal
|
||||
v-model="uploadModalVisible"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<MaterialEditModal
|
||||
v-model="editModalVisible"
|
||||
:data="currentData"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<MaterialViewModal
|
||||
v-model="viewModalVisible"
|
||||
:data="currentData"
|
||||
<MaterialForm
|
||||
v-model="drawerVisible"
|
||||
:material="currentMaterial"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Plus, RefreshCw, FileText } from '@vben/icons';
|
||||
import { ref } from 'vue';
|
||||
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';
|
||||
import { VbenButton, VbenMessage, VbenPopconfirm, VbenTag } from '@vben/common-ui';
|
||||
const drawerVisible = ref(false);
|
||||
const currentMaterial = ref<WechatMaterial | null>(null);
|
||||
|
||||
import { getWechatMaterialList, deleteWechatMaterial, syncWechatMaterial } from '#/api/core/wechat';
|
||||
import MaterialUploadModal from './modules/upload-modal.vue';
|
||||
import MaterialEditModal from './modules/edit-modal.vue';
|
||||
import MaterialViewModal from './modules/view-modal.vue';
|
||||
|
||||
import type { MaterialItem } from './data';
|
||||
import { columns, querySchema, materialTypeMap, statusMap } from './data';
|
||||
|
||||
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],
|
||||
const [VbenVxeGrid, { reload }] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
querySchema,
|
||||
queryList: async (params) => {
|
||||
const { data } = await getWechatMaterialList(params);
|
||||
return {
|
||||
data: data.list,
|
||||
total: data.total,
|
||||
};
|
||||
},
|
||||
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 colorMap: Record<string, string> = {
|
||||
image: 'blue',
|
||||
voice: 'green',
|
||||
video: 'orange',
|
||||
news: 'purple',
|
||||
};
|
||||
return colorMap[type] || 'default';
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
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('删除失败');
|
||||
const handleCellOperationClick = (code: string, row: WechatMaterial) => {
|
||||
switch (code) {
|
||||
case 'edit':
|
||||
currentMaterial.value = row;
|
||||
drawerVisible.value = true;
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete(row);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function handleSync() {
|
||||
const handleAdd = () => {
|
||||
currentMaterial.value = null;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
message.loading('正在同步微信素材...');
|
||||
await syncWechatMaterial();
|
||||
VbenMessage.success('素材同步成功');
|
||||
reloadTable();
|
||||
message.success('微信素材同步成功');
|
||||
reload();
|
||||
} catch (error) {
|
||||
VbenMessage.error('素材同步失败');
|
||||
message.error('微信素材同步失败');
|
||||
console.error('同步素材失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
gridRef.value?.reload();
|
||||
}
|
||||
const handleDelete = async (material: WechatMaterial) => {
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
export interface MenuItem {
|
||||
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';
|
||||
key?: string;
|
||||
url?: string;
|
||||
media_id?: string;
|
||||
appid?: string;
|
||||
pagepath?: string;
|
||||
media_id?: string;
|
||||
parent_id: number;
|
||||
sort: number;
|
||||
status: 0 | 1;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
export interface MenuForm {
|
||||
id?: number;
|
||||
name: string;
|
||||
type: 'click' | 'view' | 'miniprogram' | 'scancode_push' | 'scancode_waitmsg' | 'pic_sysphoto' | 'pic_photo_or_album' | 'pic_weixin' | 'location_select';
|
||||
type: string;
|
||||
key?: string;
|
||||
url?: string;
|
||||
media_id?: string;
|
||||
appid?: string;
|
||||
pagepath?: string;
|
||||
media_id?: string;
|
||||
parent_id: number;
|
||||
sort: number;
|
||||
status: 0 | 1;
|
||||
}
|
||||
|
||||
export const menuTypeOptions = [
|
||||
export const typeOptions = [
|
||||
{ label: '点击推事件', value: 'click' },
|
||||
{ label: '跳转URL', value: 'view' },
|
||||
{ label: '扫码推事件', value: 'scancode_push' },
|
||||
{ label: '扫码推事件且弹出提示', value: 'scancode_waitmsg' },
|
||||
{ label: '扫码推事件且弹出消息接收中', value: 'scancode_waitmsg' },
|
||||
{ label: '弹出系统拍照发图', value: 'pic_sysphoto' },
|
||||
{ label: '弹出拍照或者相册发图', value: 'pic_photo_or_album' },
|
||||
{ label: '弹出微信相册发图器', value: 'pic_weixin' },
|
||||
@@ -42,70 +42,60 @@ export const menuTypeOptions = [
|
||||
{ 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 = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
|
||||
export const statusMap = {
|
||||
1: '启用',
|
||||
0: '禁用',
|
||||
};
|
||||
|
||||
export const querySchema = [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '菜单名称',
|
||||
component: 'Input',
|
||||
export const gridOptions: VxeGridProps<MenuItem> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'name', title: '菜单名称', minWidth: 150, treeNode: true },
|
||||
{ field: 'type', title: '菜单类型', width: 120, formatter: ({ cellValue }) => {
|
||||
const option = typeOptions.find(item => item.value === cellValue);
|
||||
return option?.label || cellValue;
|
||||
}},
|
||||
{ field: 'key', title: '菜单KEY', width: 150 },
|
||||
{ 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,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '菜单类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: menuTypeOptions,
|
||||
placeholder: '请选择菜单类型',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// This will be implemented in the component
|
||||
return { rows: [], total: 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
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' },
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,176 +1,156 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="h-full">
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:form-options="formOptions"
|
||||
:grid-options="gridOptions"
|
||||
:grid-events="gridEvents"
|
||||
:query-schema="querySchema"
|
||||
title="自定义菜单管理"
|
||||
@toolbar-button-click="handleToolbarClick"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<VbenButton type="primary" @click="handleAdd">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
新增菜单
|
||||
</VbenButton>
|
||||
<VbenButton type="success" @click="handlePublish">
|
||||
<Upload class="mr-2 h-4 w-4" />
|
||||
<Button type="primary" @click="handleSync">
|
||||
<Icon icon="ant-design:sync-outlined" />
|
||||
同步菜单
|
||||
</Button>
|
||||
<Button type="primary" @click="handlePublish">
|
||||
<Icon icon="ant-design:cloud-upload-outlined" />
|
||||
发布菜单
|
||||
</VbenButton>
|
||||
</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>
|
||||
</Button>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<MenuEditModal
|
||||
v-model="modalVisible"
|
||||
:data="currentData"
|
||||
:parent-menus="parentMenus"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
<VbenDrawer
|
||||
v-model:show="drawerShow"
|
||||
:title="drawerTitle"
|
||||
:width="800"
|
||||
>
|
||||
<MenuForm
|
||||
:id="currentId"
|
||||
@success="handleSuccess"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</VbenDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Plus, Upload } from '@vben/icons';
|
||||
import { ref } from 'vue';
|
||||
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 { VbenButton, VbenMessage, VbenPopconfirm, VbenTag } from '@vben/common-ui';
|
||||
import { gridOptions } from './data';
|
||||
import MenuForm from './modules/menu-form.vue';
|
||||
import { deleteWechatMenu, syncWechatMenu, publishWechatMenu } from '#/api';
|
||||
|
||||
import { getWechatMenuList, deleteWechatMenu, publishWechatMenu } from '#/api/core/wechat';
|
||||
import MenuEditModal from './modules/menu-edit.vue';
|
||||
const drawerShow = ref(false);
|
||||
const drawerTitle = ref('');
|
||||
const currentId = ref<number | null>(null);
|
||||
|
||||
import type { MenuItem } from './data';
|
||||
import { columns, querySchema, menuTypeMap, statusMap } from './data';
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const currentData = ref<MenuItem | null>(null);
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
|
||||
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;
|
||||
},
|
||||
const querySchema = [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '菜单名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入菜单名称',
|
||||
},
|
||||
},
|
||||
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 = {
|
||||
// 表格事件
|
||||
};
|
||||
|
||||
const parentMenus = computed(() => {
|
||||
return menuList.value.filter(item => item.parent_id === 0);
|
||||
});
|
||||
|
||||
function getMenuTypeColor(type: string) {
|
||||
const colorMap: Record<string, string> = {
|
||||
click: 'blue',
|
||||
view: 'green',
|
||||
miniprogram: 'orange',
|
||||
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 handleToolbarClick(code: string, row: any) {
|
||||
switch (code) {
|
||||
case 'add':
|
||||
handleAdd();
|
||||
break;
|
||||
case 'edit':
|
||||
handleEdit(row);
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete(row);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
currentData.value = null;
|
||||
modalVisible.value = true;
|
||||
drawerTitle.value = '新增菜单';
|
||||
currentId.value = null;
|
||||
drawerShow.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row: MenuItem) {
|
||||
currentData.value = row;
|
||||
modalVisible.value = true;
|
||||
function handleEdit(row: any) {
|
||||
drawerTitle.value = '编辑菜单';
|
||||
currentId.value = row.id;
|
||||
drawerShow.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: MenuItem) {
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await deleteWechatMenu(row.id);
|
||||
VbenMessage.success('删除成功');
|
||||
reloadTable();
|
||||
// Refresh grid
|
||||
} catch (error) {
|
||||
VbenMessage.error('删除失败');
|
||||
console.error('删除菜单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSync() {
|
||||
try {
|
||||
await syncWechatMenu();
|
||||
// Refresh grid
|
||||
} catch (error) {
|
||||
console.error('同步菜单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublish() {
|
||||
try {
|
||||
await publishWechatMenu();
|
||||
VbenMessage.success('菜单发布成功');
|
||||
// Refresh grid
|
||||
} catch (error) {
|
||||
VbenMessage.error('菜单发布失败');
|
||||
console.error('发布菜单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
gridRef.value?.reload();
|
||||
function handleSuccess() {
|
||||
drawerShow.value = false;
|
||||
// Refresh grid
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化
|
||||
});
|
||||
function handleCancel() {
|
||||
drawerShow.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
openid: string;
|
||||
nickname: string;
|
||||
headimgurl: string;
|
||||
sex: 0 | 1 | 2; // 0:未知, 1:男, 2:女
|
||||
language: string;
|
||||
city: string;
|
||||
province: string;
|
||||
country: string;
|
||||
headimgurl: string;
|
||||
subscribe: 0 | 1; // 0:未关注, 1:已关注
|
||||
subscribe_time: string;
|
||||
unsubscribe_time?: string;
|
||||
unionid?: string;
|
||||
remark: string;
|
||||
groupid: number;
|
||||
tagid_list: string;
|
||||
subscribe_scene: string;
|
||||
qr_scene?: string;
|
||||
qr_scene_str?: string;
|
||||
remark: string;
|
||||
language: string;
|
||||
qr_scene: string;
|
||||
qr_scene_str: string;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
export interface UserForm {
|
||||
export interface WechatUserForm {
|
||||
id?: number;
|
||||
openid: string;
|
||||
remark?: string;
|
||||
remark: string;
|
||||
groupid?: number;
|
||||
}
|
||||
|
||||
@@ -37,109 +36,60 @@ export const sexOptions = [
|
||||
{ label: '女', value: 2 },
|
||||
];
|
||||
|
||||
export const sexMap = {
|
||||
0: '未知',
|
||||
1: '男',
|
||||
2: '女',
|
||||
};
|
||||
|
||||
export const subscribeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '已关注', value: 1 },
|
||||
{ label: '未关注', value: 0 },
|
||||
];
|
||||
|
||||
export const subscribeMap = {
|
||||
1: '已关注',
|
||||
0: '未关注',
|
||||
};
|
||||
|
||||
export const querySchema = [
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
label: '昵称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'openid',
|
||||
label: 'OpenID',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: sexOptions,
|
||||
placeholder: '请选择性别',
|
||||
export const gridOptions: VxeGridProps<WechatUser> = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'headimgurl', title: '头像', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue ? `<img src="${cellValue}" style="width: 40px; height: 40px; border-radius: 50%;" />` : '';
|
||||
} },
|
||||
{ field: 'nickname', title: '昵称', minWidth: 120 },
|
||||
{ field: 'sex', title: '性别', width: 80, formatter: ({ cellValue }) => {
|
||||
const option = sexOptions.find(item => item.value === cellValue);
|
||||
return option?.label || '未知';
|
||||
}},
|
||||
{ field: 'city', title: '城市', width: 100 },
|
||||
{ field: 'province', title: '省份', width: 100 },
|
||||
{ field: 'country', title: '国家', width: 100 },
|
||||
{ field: 'subscribe', title: '关注状态', width: 100, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '已关注' : '未关注';
|
||||
}},
|
||||
{ field: 'subscribe_time', title: '关注时间', width: 180 },
|
||||
{ field: 'remark', title: '备注', minWidth: 150 },
|
||||
{ field: 'groupid', title: '分组ID', width: 100 },
|
||||
{
|
||||
field: 'action',
|
||||
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],
|
||||
},
|
||||
{
|
||||
fieldName: 'subscribe',
|
||||
label: '关注状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: subscribeOptions,
|
||||
placeholder: '请选择关注状态',
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
print: true,
|
||||
refresh: true,
|
||||
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' },
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,180 +1,119 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-4">
|
||||
<VbenVxeGrid
|
||||
ref="gridRef"
|
||||
:form-options="formOptions"
|
||||
: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">
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
同步用户
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<template #avatar="{ row }">
|
||||
<VbenAvatar :src="row.headimgurl" :alt="row.nickname" size="small" />
|
||||
</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 type="default" @click="handleExport">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出用户
|
||||
</VbenButton>
|
||||
</template>
|
||||
</VbenVxeGrid>
|
||||
|
||||
<UserEditModal
|
||||
v-model="modalVisible"
|
||||
:data="currentData"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<SendMessageModal
|
||||
v-model="messageModalVisible"
|
||||
:user="currentData"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
|
||||
<MoveGroupModal
|
||||
v-model="groupModalVisible"
|
||||
:user="currentData"
|
||||
@reload="reloadTable"
|
||||
<UserForm
|
||||
v-model="drawerVisible"
|
||||
:user="currentUser"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { RefreshCw } from '@vben/icons';
|
||||
import { ref } from 'vue';
|
||||
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';
|
||||
import { VbenAvatar, VbenButton, VbenMessage, VbenTag } from '@vben/common-ui';
|
||||
const drawerVisible = ref(false);
|
||||
const currentUser = ref<WechatUser | null>(null);
|
||||
|
||||
import { getWechatUserList, syncWechatUser } from '#/api/core/wechat';
|
||||
import UserEditModal from './modules/user-edit.vue';
|
||||
import SendMessageModal from './modules/send-message.vue';
|
||||
import MoveGroupModal from './modules/move-group.vue';
|
||||
|
||||
import type { UserItem } from './data';
|
||||
import { columns, querySchema, sexMap, subscribeMap } from './data';
|
||||
|
||||
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],
|
||||
const [VbenVxeGrid, { reload }] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
querySchema,
|
||||
queryList: async (params) => {
|
||||
const { data } = await getWechatUserList(params);
|
||||
return {
|
||||
data: data.list,
|
||||
total: data.total,
|
||||
};
|
||||
},
|
||||
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 colorMap: Record<number, string> = {
|
||||
0: 'default',
|
||||
1: 'blue',
|
||||
2: 'pink',
|
||||
};
|
||||
return colorMap[sex] || 'default';
|
||||
}
|
||||
|
||||
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('用户同步失败');
|
||||
const handleCellOperationClick = (code: string, row: WechatUser) => {
|
||||
switch (code) {
|
||||
case 'edit':
|
||||
currentUser.value = row;
|
||||
drawerVisible.value = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
gridRef.value?.reload();
|
||||
}
|
||||
const handleSync = async () => {
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
117
admin-vben/apps/web-antd/src/views/finance/payment/data.ts
Normal file
117
admin-vben/apps/web-antd/src/views/finance/payment/data.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
217
admin-vben/apps/web-antd/src/views/finance/payment/list.vue
Normal file
217
admin-vben/apps/web-antd/src/views/finance/payment/list.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
103
admin-vben/apps/web-antd/src/views/log/admin/data.ts
Normal file
103
admin-vben/apps/web-antd/src/views/log/admin/data.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
187
admin-vben/apps/web-antd/src/views/log/admin/list.vue
Normal file
187
admin-vben/apps/web-antd/src/views/log/admin/list.vue
Normal 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>
|
||||
110
admin-vben/apps/web-antd/src/views/log/admin/modules/detail.vue
Normal file
110
admin-vben/apps/web-antd/src/views/log/admin/modules/detail.vue
Normal 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>
|
||||
@@ -87,17 +87,23 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ 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: 'icon',
|
||||
title: '图标',
|
||||
width: 100,
|
||||
slots: { default: 'icon' },
|
||||
align: 'center',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue ? `<i class="${cellValue}" style="font-size: 24px;"></i>` : '';
|
||||
},
|
||||
},
|
||||
{ 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: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -105,6 +111,18 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -91,11 +91,15 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ 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: 'sign_name', title: '签名名称', width: 150 },
|
||||
{ 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: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -103,6 +107,18 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -105,11 +105,17 @@ export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ 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: '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: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'status', title: '状态', width: 80, formatter: ({ cellValue }) => {
|
||||
return cellValue === 1 ? '启用' : '禁用';
|
||||
} },
|
||||
{ field: 'create_time', title: '创建时间', width: 180 },
|
||||
{ field: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -117,6 +123,18 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -91,12 +91,19 @@ export const querySchema = [
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{ 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_value', title: '配置值', minWidth: 200, showOverflow: true },
|
||||
{ field: 'config_desc', title: '配置描述', minWidth: 200, showOverflow: true },
|
||||
{ field: 'is_system', title: '系统配置', width: 100, slots: { default: 'isSystem' } },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'is_system', 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: 'update_time', title: '更新时间', width: 180 },
|
||||
{
|
||||
@@ -104,6 +111,17 @@ export const columns: VxeGridProps['columns'] = [
|
||||
title: '操作',
|
||||
width: 150,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
116
admin-vben/apps/web-antd/src/views/setting/system/data.ts
Normal file
116
admin-vben/apps/web-antd/src/views/setting/system/data.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
204
admin-vben/apps/web-antd/src/views/setting/system/list.vue
Normal file
204
admin-vben/apps/web-antd/src/views/setting/system/list.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -195,77 +195,47 @@ export function useColumns(
|
||||
title: $t('site.list.status'),
|
||||
field: 'status',
|
||||
width: 100,
|
||||
slots: {
|
||||
default: ({ row }) => (
|
||||
<Tag
|
||||
color={getStatusColor(row.status)}
|
||||
class="cursor-pointer"
|
||||
onClick={() => onStatusChange(row.status, row.site_id)}
|
||||
>
|
||||
{row.status_name}
|
||||
</Tag>
|
||||
),
|
||||
formatter: ({ row }) => {
|
||||
const colorMap: Record<number, string> = {
|
||||
0: 'red',
|
||||
1: 'green',
|
||||
2: 'orange',
|
||||
3: 'red',
|
||||
};
|
||||
const color = colorMap[row.status] || 'default';
|
||||
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'),
|
||||
field: 'action',
|
||||
width: 280,
|
||||
slots: {
|
||||
default: ({ row }) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleToSiteLink(row.site_id)}
|
||||
>
|
||||
<Icon icon="ant-design:login-outlined" />
|
||||
{$t('site.list.toSite')}
|
||||
</Button>
|
||||
{(row.status === 1 || row.status === 3) && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => onOpenClose(row.status, row.site_id)}
|
||||
>
|
||||
{row.status === 1 ? $t('site.list.close') : $t('site.list.open')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => onActionClick('info', row)}
|
||||
>
|
||||
<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>
|
||||
),
|
||||
cellRender: {
|
||||
name: 'CellOperation',
|
||||
attrs: {
|
||||
onClick: (code: string, row: any) => {
|
||||
if (code === 'toSite') {
|
||||
window.open(`/site/${row.site_id}`, '_blank');
|
||||
} else if (code === 'toggleStatus') {
|
||||
onOpenClose(row.status, row.site_id);
|
||||
} else if (code === 'info') {
|
||||
onActionClick('info', row);
|
||||
} else if (code === 'init') {
|
||||
onActionClick('init', row);
|
||||
} else if (code === 'edit') {
|
||||
onActionClick('edit', row);
|
||||
} else if (code === 'delete') {
|
||||
onActionClick('delete', row);
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{ code: 'toSite', text: $t('site.list.toSite'), icon: 'ant-design:login-outlined' },
|
||||
{ code: 'info', text: $t('site.list.info'), icon: 'ant-design:info-circle-outlined' },
|
||||
{ code: 'init', text: $t('site.list.initSite'), icon: 'ant-design:reload-outlined' },
|
||||
{ code: 'edit', text: $t('common.edit'), icon: 'ant-design:edit-outlined' },
|
||||
{ code: 'delete', text: $t('common.delete'), icon: 'ant-design:delete-outlined', danger: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -29,10 +29,12 @@ export function useColumns(
|
||||
align: 'left',
|
||||
field: 'meta.title',
|
||||
fixed: 'left',
|
||||
slots: { default: 'title' },
|
||||
title: $t('system.menu.menuTitle'),
|
||||
treeNode: true,
|
||||
width: 250,
|
||||
formatter: ({ row }) => {
|
||||
return row.meta?.title || '';
|
||||
},
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
|
||||
99
admin-vben/apps/web-antd/src/views/tools/backup/data.ts
Normal file
99
admin-vben/apps/web-antd/src/views/tools/backup/data.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
219
admin-vben/apps/web-antd/src/views/tools/backup/list.vue
Normal file
219
admin-vben/apps/web-antd/src/views/tools/backup/list.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -263,6 +263,7 @@ const notificationEl : any = null
|
||||
|
||||
const elNotificationClick = () => {
|
||||
showDialog.value = true
|
||||
cloudBuildModalApi.open()
|
||||
active.value = 'build'
|
||||
getCloudBuildLogFn()
|
||||
}
|
||||
@@ -284,18 +285,22 @@ const open = async () => {
|
||||
loading.value = false
|
||||
cloudBuildTask.value = data
|
||||
showDialog.value = true
|
||||
cloudBuildModalApi.open()
|
||||
getCloudBuildLogFn()
|
||||
}).catch(() => {
|
||||
showDialog.value = false
|
||||
cloudBuildModalApi.close()
|
||||
loading.value = false
|
||||
})
|
||||
} else {
|
||||
loading.value = false
|
||||
cloudBuildCheck.value = data
|
||||
showDialog.value = true
|
||||
cloudBuildModalApi.open()
|
||||
}
|
||||
}).catch(() => {
|
||||
showDialog.value = false
|
||||
cloudBuildModalApi.close()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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" >
|
||||
<div v-loading="loading">
|
||||
<div class="text-page-title mb-[20px]">历史版本</div>
|
||||
@@ -28,12 +28,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-dialog>
|
||||
</ModalUpgradeLog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, defineProps, nextTick } from "vue"
|
||||
import { t } from "@/lang"
|
||||
import { getAppVersionList, getFrameworkVersionList } from "@/app/api/module"
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const props = defineProps({
|
||||
upgradeKey: {
|
||||
@@ -65,6 +66,7 @@ const getAppVersionListFn = () => {
|
||||
return
|
||||
}else{
|
||||
dialogVisible.value = true
|
||||
upgradeLogModalApi.open()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -80,6 +82,7 @@ const getFrameworkVersionListFn = () => {
|
||||
})
|
||||
frameworkVersionList.value = data
|
||||
dialogVisible.value = true
|
||||
upgradeLogModalApi.open()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -88,6 +91,7 @@ const activeName = ref(0)
|
||||
// 提交信息
|
||||
const loading = ref(true)
|
||||
const dialogVisible = ref(false)
|
||||
const [ModalUpgradeLog, upgradeLogModalApi] = useVbenModal()
|
||||
const open = async () => {
|
||||
nextTick(() => {
|
||||
activeName.value = 0 // 重置
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<!-- 检测服务是否到期 -->
|
||||
@@ -203,9 +203,9 @@
|
||||
</template>
|
||||
</div>
|
||||
</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>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
@@ -214,9 +214,9 @@
|
||||
<el-button @click="upgradeTipsShowDialog = false">{{ t("cancel") }}</el-button>
|
||||
</div>
|
||||
</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>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
@@ -225,7 +225,7 @@
|
||||
<el-button @click="cloudBuildError('rollback')" type="primary">{{ t("upgrade.rollback") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalCloudBuildError>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -544,6 +544,7 @@ const open = (addonKey: string = '', callback = null) => {
|
||||
handleUpgrade()
|
||||
} else {
|
||||
upgradeTipsShowDialog.value = true
|
||||
upgradeTipsModalApi.open()
|
||||
}
|
||||
if (callback) callback()
|
||||
}).catch(() => {
|
||||
@@ -631,6 +632,7 @@ const clearUpgradeTaskFn = () => {
|
||||
*/
|
||||
const cloudBuildError = (event: string) => {
|
||||
cloudBuildErrorTipsShowDialog.value = false
|
||||
cloudErrorModalApi.close()
|
||||
switch (event) {
|
||||
case 'local':
|
||||
upgradeUserOperate(event).then(() => {
|
||||
@@ -658,7 +660,11 @@ const timeSplit = (str: string) => {
|
||||
const upgradeTipsConfirm = (isLock: boolean = false) => {
|
||||
isLock && Storage.set({ key: 'upgradeTipsLock', data: isLock })
|
||||
upgradeTipsShowDialog.value = false
|
||||
!isLock && (showDialog.value = true)
|
||||
upgradeTipsModalApi.close()
|
||||
if (!isLock) {
|
||||
showDialog.value = true
|
||||
upgradeModalApi.open()
|
||||
}
|
||||
}
|
||||
const activeName = ref(0)
|
||||
const numberOfSteps = ref(0)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('headImg')">
|
||||
<upload-image v-model="saveInfo.head_img" :limit="1" imageFit="cover" />
|
||||
@@ -14,10 +14,10 @@
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalUserInfoEdit>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
@@ -26,6 +26,7 @@ import type { FormInstance } from 'element-plus'
|
||||
import { deepClone } from '@/utils/common'
|
||||
import { getUserInfo, setUserInfo } from '@/app/api/personal'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const userStore = useUserStore()
|
||||
// 提交信息
|
||||
@@ -33,6 +34,7 @@ const saveInfo: any = reactive({})
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(true)
|
||||
const dialogVisible = ref(false)
|
||||
const [ModalUserInfoEdit, userInfoModalApi] = useVbenModal()
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
@@ -48,6 +50,7 @@ const getUserInfoFn = () => {
|
||||
getUserInfoFn()
|
||||
const open = ()=>{
|
||||
dialogVisible.value = true
|
||||
userInfoModalApi.open()
|
||||
getUserInfoFn()
|
||||
}
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
@@ -58,6 +61,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
loading.value = false
|
||||
dialogVisible.value = false
|
||||
userInfoModalApi.close()
|
||||
let data: any = deepClone(userStore.userInfo)
|
||||
data.head_img = saveInfo.head_img
|
||||
userStore.setUserInfo(data)
|
||||
@@ -69,6 +73,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
const cancel = () => { dialogVisible.value = false; userInfoModalApi.close() }
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-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" />
|
||||
@@ -29,11 +29,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalRole>
|
||||
</template>
|
||||
|
||||
<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 { debounce } from '@/utils/common'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalRole, modalApi] = useVbenModal()
|
||||
const loading = ref(false)
|
||||
const isOpen = ref(true)
|
||||
|
||||
@@ -148,7 +148,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
save(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
modalApi.close()
|
||||
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
@@ -184,6 +184,7 @@ const setFormData = async (row: any = null) => {
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
function checked (menuKey:string, data:any, newArr:any) {
|
||||
@@ -201,10 +202,9 @@ function checked (menuKey:string, data:any, newArr:any) {
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('accountNumber')" v-if="!formData.uid" prop="uid">
|
||||
@@ -53,11 +53,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalUser>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -95,7 +95,7 @@ const needAddUserInfo = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalUser, modalApi] = useVbenModal()
|
||||
const loading = ref(false)
|
||||
let popTitle: string = ''
|
||||
|
||||
@@ -181,7 +181,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formData.uid && typeof uid.value == 'number') data.uid = uid.value
|
||||
save(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
modalApi.close()
|
||||
!formData.uid && getUserList()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
@@ -206,12 +206,12 @@ const setFormData = async (row: any = null) => {
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
<template #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">
|
||||
<el-button type="primary" class="ml-3" @click="step = 2">{{
|
||||
t('next')
|
||||
@@ -116,7 +116,7 @@
|
||||
</view>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalAppVersionEdit>
|
||||
|
||||
<generate-sing-cert ref="generateSingCertRef"/>
|
||||
</template>
|
||||
@@ -127,8 +127,9 @@ import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { addVersion, editVersion, getVersionInfo, getAppPlatform } from '@/app/api/app'
|
||||
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 appPlatform = ref({})
|
||||
const step = ref(1)
|
||||
@@ -242,7 +243,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
save(formData).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
versionModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -263,7 +264,7 @@ const setFormData = async (row: any = null) => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(() => showDialog.value, () => {
|
||||
watch(() => versionModalApi.getVisible?.(), () => {
|
||||
step.value = 1
|
||||
})
|
||||
|
||||
@@ -271,10 +272,8 @@ const windowOpen = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { versionModalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item label="证书别名" prop="key_alias">
|
||||
@@ -62,11 +62,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalGenerateCert>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -74,8 +74,9 @@ import { ref, reactive, computed } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { generateSingCert } from '@/app/api/app'
|
||||
import { img } from '@/utils/common'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalGenerateCert, certModalApi] = useVbenModal()
|
||||
const moreInfo = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -137,7 +138,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
generateSingCert(formData).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
certModalApi.close()
|
||||
window.open(img(res.data), '_blank')
|
||||
})
|
||||
}
|
||||
@@ -145,12 +146,11 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
const open = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
showDialog.value = true
|
||||
certModalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
const cancel = () => { certModalApi.close() }
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
</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-item prop="code" :label="t('code')">
|
||||
<el-input v-model.trim="form.code" :placeholder="t('codePlaceholder')" onkeyup="this.value = this.value.replace(/[^\d\.]/g,'');" />
|
||||
@@ -68,29 +68,29 @@
|
||||
</el-form>
|
||||
<template #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">
|
||||
{{ t('confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</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]">
|
||||
<div v-html="failReason"></div>
|
||||
</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>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalUploadSuccess>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -105,11 +105,13 @@ import { ElMessageBox } from 'element-plus'
|
||||
import { AnyObject } from '@/types/global'
|
||||
import Storage from '@/utils/storage'
|
||||
import { siteWeappCommit, undoAudit } from "@/app/api/wxoplatform";
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const dialogVisible = ref(false)
|
||||
const [ModalWeappRelease, releaseModalApi] = useVbenModal()
|
||||
const loading = ref(true)
|
||||
const weappTableData:{
|
||||
page: number,
|
||||
@@ -131,7 +133,9 @@ const form = ref({
|
||||
content: ''
|
||||
})
|
||||
const uploadSuccessShowDialog = ref(false)
|
||||
const [ModalUploadSuccess, uploadSuccessModalApi] = useVbenModal()
|
||||
const authCode = ref('')
|
||||
const [ModalFailReason, failReasonModalApi] = useVbenModal()
|
||||
|
||||
getAuthInfo().then(res => {
|
||||
if (res.data.data && res.data.data.auth_code) {
|
||||
@@ -187,6 +191,7 @@ getWeappVersionListFn()
|
||||
const handleClose = () => {
|
||||
ruleFormRef.value.clearValidate()
|
||||
}
|
||||
const releaseCancel = () => { releaseModalApi.close() }
|
||||
|
||||
const uploading = ref(false)
|
||||
const insert = () => {
|
||||
@@ -239,7 +244,7 @@ const getWeappUploadLogFn = (key: string) => {
|
||||
if (last.code == 1 && last.percent == 100) {
|
||||
getWeappVersionListFn()
|
||||
getWeappPreviewImage()
|
||||
!Storage.get('weappUploadTipsLock') && (uploadSuccessShowDialog.value = true)
|
||||
!Storage.get('weappUploadTipsLock') && uploadSuccessModalApi.open()
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
@@ -308,12 +313,15 @@ const failReasonDialogVisible = ref(false)
|
||||
const handleFailReason = (data: any) => {
|
||||
failReason.value = data.fail_reason
|
||||
failReasonDialogVisible.value = true
|
||||
failReasonModalApi.open()
|
||||
}
|
||||
|
||||
const knownToKnow = () => {
|
||||
Storage.set({ key: 'weappUploadTipsLock', data: true })
|
||||
uploadSuccessShowDialog.value = false
|
||||
uploadSuccessModalApi.close()
|
||||
}
|
||||
const uploadSuccessCancel = () => { uploadSuccessModalApi.close() }
|
||||
|
||||
const againUpload = () => {
|
||||
if (uploading.value) return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('requestUrl')" prop="requestdomain">
|
||||
<el-input v-model="formData.requestdomain" :placeholder="t('requestdomainPlaceholder')" type="textarea">
|
||||
@@ -29,11 +29,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalWeappDomain>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -42,8 +42,9 @@ import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { setWeappDomain } from '@/app/api/weapp'
|
||||
import Test from '@/utils/test'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalWeappDomain, domainModalApi] = useVbenModal()
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
@@ -135,7 +136,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
setWeappDomain(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
domainModalApi.close()
|
||||
emit('complete', data)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -154,10 +155,8 @@ const setFormData = async (data: any = null) => {
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { domainModalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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]">
|
||||
<el-scrollbar>
|
||||
<el-form :model="formData" label-width="auto" label-position="left" ref="formRef" :rules="formRules"
|
||||
@@ -95,11 +95,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalWeappPrivacy>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -108,8 +108,9 @@ import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import SettingList from './setting-list.vue'
|
||||
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 settingListRef = ref(null)
|
||||
const sdkSettingListRef = ref(null)
|
||||
@@ -187,7 +188,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
setWeappPrivacySetting(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
privacyModalApi.close()
|
||||
emit('complete', data)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -209,14 +210,12 @@ const setFormData = async () => {
|
||||
formData.store_expire_timestamp = data.owner_setting.store_expire_timestamp
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
privacyModalApi.open()
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { privacyModalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
</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">
|
||||
<template v-for="(item, index) in privacyList">
|
||||
<el-checkbox :label="item.privacy_key" v-if="!checkIsSelected(item.privacy_key)">{{ item.privacy_text }}</el-checkbox>
|
||||
@@ -122,16 +122,17 @@
|
||||
</el-checkbox-group>
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalSettingType>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -142,6 +143,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const [ModalSettingType, settingModalApi] = useVbenModal()
|
||||
const settingTypeDialog = ref(false)
|
||||
|
||||
const privacyList = ref([
|
||||
@@ -199,11 +201,13 @@ const selectSettingType = () => {
|
||||
})
|
||||
})
|
||||
settingTypeDialog.value = false
|
||||
settingModalApi.close()
|
||||
checkList.value = []
|
||||
}
|
||||
|
||||
const addSettingList = () => {
|
||||
settingTypeDialog.value = true
|
||||
settingModalApi.open()
|
||||
}
|
||||
|
||||
const selectedSettingType = computed(() => {
|
||||
@@ -219,3 +223,4 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
const cancel = () => { settingTypeDialog.value = false; settingModalApi.close() }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div @click="openDialog">
|
||||
<slot></slot>
|
||||
</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="attachment-list-wrap flex flex-col p-[15px] flex-1 overflow-hidden">
|
||||
@@ -88,11 +88,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalWechatMedia>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -101,6 +101,7 @@ import { t } from '@/lang'
|
||||
import UploadMedia from './upload-media.vue'
|
||||
import { img, debounce } from '@/utils/common'
|
||||
import { getMediaList, syncNews } from '@/app/api/wechat'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const prop = defineProps({
|
||||
type: {
|
||||
@@ -109,11 +110,11 @@ const prop = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalWechatMedia, mediaModalApi] = useVbenModal()
|
||||
|
||||
const openDialog = () => {
|
||||
prop.type == 'news' && waterfall()
|
||||
showDialog.value = true
|
||||
mediaModalApi.open()
|
||||
}
|
||||
|
||||
const attachment: Record<string, any> = reactive({
|
||||
@@ -150,7 +151,9 @@ const selectedFile: Record<string, any> = ref({})
|
||||
|
||||
const confirm = () => {
|
||||
emits('success', selectedFile.value)
|
||||
mediaModalApi.close()
|
||||
}
|
||||
const cancel = () => { mediaModalApi.close() }
|
||||
|
||||
const syncLoading = ref(false)
|
||||
const syncWechatNews = () => {
|
||||
|
||||
@@ -66,16 +66,16 @@
|
||||
</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"/>
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalReplyContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,6 +88,7 @@ import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ReplyForm from '@/app/views/channel/wechat/components/reply-form.vue'
|
||||
import NewsCard from '@/app/views/channel/wechat/components/news-card.vue'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -96,6 +97,7 @@ const back = () => {
|
||||
router.push('/channel/wechat/reply')
|
||||
}
|
||||
|
||||
const [ModalReplyContent, replyModalApi] = useVbenModal()
|
||||
const showDialog = ref(false)
|
||||
|
||||
const formData: any = reactive({
|
||||
@@ -116,9 +118,11 @@ const addReplyContent = () => {
|
||||
formData.content.push(replyContent.value)
|
||||
replyContent.value = {}
|
||||
showDialog.value = false
|
||||
replyModalApi.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
const cancelReply = () => { showDialog.value = false; replyModalApi.close() }
|
||||
|
||||
const removeContent = (index: number) => {
|
||||
formData.content.splice(index, 1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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]">
|
||||
<el-button type="primary" @click="addEvent">
|
||||
{{ t('addDictData') }}
|
||||
@@ -20,11 +20,11 @@
|
||||
</el-table>
|
||||
<template #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>
|
||||
</span>
|
||||
</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-item :label="t('name')">
|
||||
<el-input v-model.trim="name" disabled class="input-width" />
|
||||
@@ -47,12 +47,12 @@
|
||||
</el-form>
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</ModalDictItem>
|
||||
</ModalDict>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -61,10 +61,11 @@ import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { setDictData, getDictInfo } from '@/app/api/dict'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalDict, dictModalApi] = useVbenModal()
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const [ModalDictItem, itemModalApi] = useVbenModal()
|
||||
|
||||
const tableDate = ref<Array<any>>([])
|
||||
const id = ref()
|
||||
@@ -95,7 +96,7 @@ const formRules = computed(() => {
|
||||
const addEvent = () => {
|
||||
type.value = 'add'
|
||||
formData.value = cloneDeep(initialFormData)
|
||||
dialogVisible.value = true
|
||||
itemModalApi.open()
|
||||
}
|
||||
const tableIndex = ref(0)
|
||||
const editEvent = (row: any, index: number) => {
|
||||
@@ -103,7 +104,7 @@ const editEvent = (row: any, index: number) => {
|
||||
tableIndex.value = index
|
||||
formData.value = cloneDeep(initialFormData)
|
||||
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.sort(function (a, b) { return b.sort - a.sort })
|
||||
dialogVisible.value = false
|
||||
itemModalApi.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -137,7 +138,7 @@ const confirm = async () => {
|
||||
loading.value = true
|
||||
setDictData(id.value, { dictionary: JSON.stringify(tableDate.value) }).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
dictModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -145,7 +146,7 @@ const confirm = async () => {
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
showDialog.value = true
|
||||
dictModalApi.open()
|
||||
loading.value = true
|
||||
id.value = row.id
|
||||
name.value = row.name
|
||||
@@ -154,10 +155,9 @@ const setFormData = async (row: any = null) => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { dictModalApi.close() }
|
||||
const itemCancel = () => { itemModalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="formData.id ? t('updateDict') : t('addDict')" width="480" class="diy-dialog-wrap" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('name')" prop="name">
|
||||
<el-input v-model.trim="formData.name" clearable maxlength="40" show-word-limit :placeholder="t('namePlaceholder')" class="input-width" />
|
||||
</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>
|
||||
<Modal :class="'w-[480px]'" :title="formModel.id ? t('updateDict') : t('addDict')">
|
||||
<BaseForm />
|
||||
<p class="form-tip">{{ t('keyFormatTips') }}</p>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -40,80 +23,41 @@ const initialFormData = {
|
||||
key: '',
|
||||
memo: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
name: [
|
||||
{ required: true, message: t('namePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
key: [
|
||||
{ required: true, message: t('keyPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (/^[a-zA-Z_]+$/.test(value)) {
|
||||
callback()
|
||||
} else {
|
||||
callback(new Error(t('keyFormatTips')))
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
data: [
|
||||
{ required: true, message: t('dataPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
const [Modal, modalApi] = useVbenModal()
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
const save = values.id ? editDict : addDict
|
||||
save(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{ 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')) } ] },
|
||||
{ component: 'Textarea', fieldName: 'memo', label: t('memo') }
|
||||
],
|
||||
wrapperClass: 'grid-cols-1'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @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 confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
loading.value = true
|
||||
if (row) {
|
||||
const data = await (await getDictInfo(row.id)).data
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
Object.assign(formModel, initialFormData)
|
||||
loading.value = true
|
||||
if (row) {
|
||||
const data = await (await getDictInfo(row.id)).data
|
||||
if (data) { Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] }) }
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</el-form>
|
||||
</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 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>
|
||||
<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>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</ModalStyleSelect>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
@@ -178,6 +178,7 @@ import { t } from '@/lang'
|
||||
import { watch, ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { img } from '@/utils/common'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
|
||||
@@ -212,9 +213,9 @@ watch(
|
||||
}, { deep: true }
|
||||
)
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalStyleSelect, styleModalApi] = useVbenModal()
|
||||
const showStyle = () => {
|
||||
showDialog.value = true
|
||||
styleModalApi.open()
|
||||
}
|
||||
|
||||
const selectStyle = ref('style-1')
|
||||
@@ -234,8 +235,9 @@ const changeStyle = () => {
|
||||
break
|
||||
}
|
||||
diyStore.global.topStatusBar.style = selectStyle.value
|
||||
showDialog.value = false
|
||||
styleModalApi.close()
|
||||
}
|
||||
const cancel = () => { styleModalApi.close() }
|
||||
|
||||
const selectImg = (url: any) => {
|
||||
const image = new Image()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item label="色调名称" prop="title">
|
||||
<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" />
|
||||
<template #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" @click="confirmFn(formRef)">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalEditTheme>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -51,10 +51,11 @@ import addThemeComponent from './add-theme.vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { addTheme, editTheme } from '@/app/api/diy'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
|
||||
const dialogThemeVisible = ref(false)
|
||||
const [ModalEditTheme, themeModalApi] = useVbenModal()
|
||||
const addThemeRef = ref()
|
||||
const openData: Record<string, any> = reactive({ // 用于接收弹窗打开时的参数
|
||||
title: '',
|
||||
@@ -93,7 +94,7 @@ const open = (res: any) => { // 参数: title=>色调名称,key=>区分系
|
||||
formData.value.forEach((item, index) => {
|
||||
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) => {
|
||||
confirmRepeat = false
|
||||
dialogThemeVisible.value = false
|
||||
themeModalApi.close()
|
||||
emit('confirm', params)
|
||||
}).catch(() => {
|
||||
confirmRepeat = false
|
||||
@@ -226,6 +227,7 @@ const confirmFn = async (formEl: FormInstance | undefined) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
const cancel = () => { themeModalApi.close() }
|
||||
|
||||
const applyOpacity = (color, opacity) => {
|
||||
// 解析十六进制或 RGBA 格式
|
||||
@@ -258,10 +260,7 @@ const colorPickerChange = (e: any, data: any) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
defineExpose({
|
||||
dialogThemeVisible,
|
||||
open
|
||||
})
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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="preview-wrap">
|
||||
@@ -209,7 +209,7 @@
|
||||
|
||||
<template #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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -227,7 +227,9 @@ import storage from '@/utils/storage'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { getFormSubmitConfig,editDiyFormSubmitConfig } from '@/app/api/diy_form'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const [ModalFormSubmit, formSubmitModalApi] = useVbenModal()
|
||||
const showDialog = ref(false)
|
||||
const repeat = ref(false)
|
||||
|
||||
@@ -350,12 +352,15 @@ const confirm = () => {
|
||||
editDiyFormSubmitConfig(data).then(res => {
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
formSubmitModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(err => {
|
||||
repeat.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const cancel = () => { showDialog.value = false; formSubmitModalApi.close() }
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
|
||||
@@ -108,11 +108,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalFormWrite>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -122,7 +122,9 @@ import type { FormInstance } from 'element-plus'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
import {getMemberLabelAll,getMemberLevelAll } from '@/app/api/member'
|
||||
import { getFormWriteConfig,editDiyFormWriteConfig } from '@/app/api/diy_form'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const [ModalFormWrite, formWriteModalApi] = useVbenModal()
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -308,6 +310,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
editDiyFormWriteConfig(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
formWriteModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(err => {
|
||||
loading.value = false
|
||||
@@ -325,6 +328,7 @@ defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { showDialog.value = false; formWriteModalApi.close() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</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-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" />-->
|
||||
@@ -120,14 +120,14 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</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-tab-pane :label="t('wechat')" name="wechat"></el-tab-pane>
|
||||
<el-tab-pane :label="t('weapp')" name="weapp"></el-tab-pane>
|
||||
@@ -149,11 +149,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalShareSet>
|
||||
|
||||
<!-- 推广弹出框 -->
|
||||
<spread-popup ref="spreadPopupRef" />
|
||||
@@ -179,6 +179,7 @@ import { getFormType, getApps, getDiyFormPageList, deleteDiyForm, editDiyFormSha
|
||||
import { FormInstance, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { setTablePageStorage, getTablePageStorage, img } from '@/utils/common'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
import recordsDetail from '@/app/views/diy_form/records.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 [ModalAddFormTips, addFormModalApi] = useVbenModal()
|
||||
const dialogVisible = ref(false)
|
||||
const addEvent = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
@@ -232,11 +234,13 @@ const addEvent = async (formEl: FormInstance | undefined) => {
|
||||
})
|
||||
window.open(url.href)
|
||||
dialogVisible.value = false
|
||||
addFormModalApi.close()
|
||||
formData.title = ''
|
||||
formData.type = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
const cancelAddForm = () => { dialogVisible.value = false; addFormModalApi.close() }
|
||||
|
||||
const showClick = (row: any) => {
|
||||
const data = row.status === 1 ? 0 : 1
|
||||
@@ -472,6 +476,7 @@ const shareFormData = reactive({
|
||||
}
|
||||
})
|
||||
|
||||
const [ModalShareSet, shareModalApi] = useVbenModal()
|
||||
const shareDialogVisible = ref(false)
|
||||
const shareFormRules = computed(() => {
|
||||
return {}
|
||||
@@ -486,6 +491,7 @@ const openShare = async (row: any) => {
|
||||
shareFormData.weapp = share.weapp
|
||||
|
||||
shareDialogVisible.value = true
|
||||
shareModalApi.open()
|
||||
}
|
||||
|
||||
const shareEvent = async (formEl: FormInstance | undefined) => {
|
||||
@@ -499,11 +505,13 @@ const shareEvent = async (formEl: FormInstance | undefined) => {
|
||||
}).then(() => {
|
||||
loadDiyFormList(getTablePageStorage(diyFormTableData.searchParam).page)
|
||||
shareDialogVisible.value = false
|
||||
shareModalApi.close()
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const cancelShare = () => { shareDialogVisible.value = false; shareModalApi.close() }
|
||||
|
||||
// 表单推广
|
||||
const spreadPopupRef = ref(null)
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
</el-tab-pane>
|
||||
</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 mb-[10px]" v-for="(item, index) in formDetail" :key="index">
|
||||
<div class="flex justify-end w-[100px]">{{ item.label }}:</div>
|
||||
@@ -167,10 +167,10 @@
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalDiyFormView>
|
||||
|
||||
</el-drawer>
|
||||
<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 activeName = ref('detail_data')
|
||||
const formId = ref(0)
|
||||
const [ModalDiyFormView, viewModalApi] = useVbenModal()
|
||||
const dialogVisible = ref(false)
|
||||
const searchFormDiyFormRef = ref<FormInstance>()
|
||||
const searchFormDiyMemberRef = ref<FormInstance>()
|
||||
@@ -241,8 +242,10 @@ const formDetailEvent = (row: any) => {
|
||||
getFormRecordsInfo(row.record_id).then((res:any) => {
|
||||
formDetail.value = res.data.value
|
||||
dialogVisible.value = true
|
||||
viewModalApi.open()
|
||||
})
|
||||
}
|
||||
const closeView = () => { dialogVisible.value = false; viewModalApi.close() }
|
||||
|
||||
// 删除
|
||||
const deleteEvent = (row: any) => {
|
||||
@@ -388,3 +391,4 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
</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-item :label="t('tradeNo')">-->
|
||||
@@ -159,10 +159,10 @@
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalAccountDetail>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -232,6 +232,7 @@ const checkAccountType = () => {
|
||||
})
|
||||
}
|
||||
checkAccountType()
|
||||
const [ModalAccountDetail, accountDetailModalApi] = useVbenModal()
|
||||
const showDialog = ref(false)
|
||||
const formData = ref({
|
||||
trade_no: '',
|
||||
@@ -253,8 +254,10 @@ const formData = ref({
|
||||
})
|
||||
const detailEvent = (info:any) => {
|
||||
showDialog.value = true
|
||||
accountDetailModalApi.open()
|
||||
formData.value = info
|
||||
}
|
||||
const cancel = () => { showDialog.value = false; accountDetailModalApi.close() }
|
||||
|
||||
interface AccountStat{
|
||||
pay: number,
|
||||
@@ -274,3 +277,4 @@ checkAccountStat()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
</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-row>
|
||||
<el-col :span="12">
|
||||
@@ -309,12 +309,12 @@
|
||||
|
||||
<template #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>
|
||||
</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-row>
|
||||
<el-col :span="12">
|
||||
@@ -390,13 +390,13 @@
|
||||
</el-form>
|
||||
<template #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>
|
||||
</span>
|
||||
</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-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" />
|
||||
@@ -404,14 +404,14 @@
|
||||
</el-form>
|
||||
<template #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>
|
||||
</span>
|
||||
</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-row>
|
||||
<template v-if="transferData.transfer_type == 'alipay' || transferData.transfer_type == 'wechat_code'">
|
||||
@@ -476,13 +476,13 @@
|
||||
</el-form>
|
||||
<template #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>
|
||||
</span>
|
||||
</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-item :label="t('remark')" prop="remark">
|
||||
<el-input v-model.trim="formData.remark" type="textarea" rows="4" clearable
|
||||
@@ -491,11 +491,11 @@
|
||||
</el-form>
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalRemark>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -506,6 +506,7 @@ import { getCashOutList, getTransfertype, memberTransfer, memberAudit, getCashOu
|
||||
import { img } from '@/utils/common'
|
||||
import { ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const cashOutStatusList = ref([])
|
||||
const checkStatusList = async () => {
|
||||
@@ -608,28 +609,29 @@ const formTransferRules = computed(() => {
|
||||
const transferFn = (data:any) => {
|
||||
transferData.value = data
|
||||
formTransfer.id = data.id
|
||||
transferShowDialog.value = true
|
||||
modalTransferApi.open()
|
||||
}
|
||||
const handleTransfer = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
memberTransfer({ ...formTransfer }).then(res => {
|
||||
transferShowDialog.value = false
|
||||
modalTransferApi.close()
|
||||
loadOrderList()
|
||||
}).catch(() => {
|
||||
transferShowDialog.value = false
|
||||
modalTransferApi.close()
|
||||
loadOrderList()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const transferCancel = () => { modalTransferApi.close() }
|
||||
|
||||
/**
|
||||
* 详情
|
||||
* @param data
|
||||
*/
|
||||
const cashOutShowDialog = ref(false)
|
||||
const [ModalDetail, modalDetailApi] = useVbenModal()
|
||||
const cashOutInfo = ref({
|
||||
nickname: '',
|
||||
account_type_name: '',
|
||||
@@ -643,25 +645,26 @@ const cashOutLoading = ref(true)
|
||||
const detailFn = (id:any) => {
|
||||
getCashOutDetail(id).then(res => {
|
||||
cashOutInfo.value = res.data
|
||||
cashOutShowDialog.value = true
|
||||
modalDetailApi.open()
|
||||
cashOutLoading.value = false
|
||||
}).catch(() => {
|
||||
loadOrderList()
|
||||
})
|
||||
}
|
||||
const detailClose = () => { modalDetailApi.close() }
|
||||
|
||||
/**
|
||||
* 提现审核
|
||||
* @param data
|
||||
*/
|
||||
|
||||
const auditPassShowDialog = ref(false)
|
||||
const [ModalAuditPass, modalAuditPassApi] = useVbenModal()
|
||||
const curData = ref<any>({})
|
||||
|
||||
// 审核成功弹框
|
||||
const successfulAuditFn = (data: any) => {
|
||||
curData.value = data
|
||||
auditPassShowDialog.value = true
|
||||
modalAuditPassApi.open()
|
||||
}
|
||||
const handlePass = () => {
|
||||
const obj = {
|
||||
@@ -670,24 +673,26 @@ const handlePass = () => {
|
||||
}
|
||||
cashOutAuditFn(obj)
|
||||
}
|
||||
const auditPassCancel = () => { modalAuditPassApi.close() }
|
||||
|
||||
/**
|
||||
* 拒绝审核
|
||||
*/
|
||||
|
||||
const auditFailure = ref({ refuse_reason: '', id: 0, action: '' })
|
||||
const auditShowDialog = ref(false)
|
||||
const [ModalAuditRefuse, modalAuditRefuseApi] = useVbenModal()
|
||||
const loading = ref(false)
|
||||
|
||||
const auditFailureFn = (data: any) => {
|
||||
auditFailure.value.id = data.id
|
||||
auditFailure.value.action = 'refuse'
|
||||
auditShowDialog.value = true
|
||||
modalAuditRefuseApi.open()
|
||||
}
|
||||
const confirm = () => {
|
||||
auditShowDialog.value = false
|
||||
modalAuditRefuseApi.close()
|
||||
cashOutAuditFn(auditFailure.value)
|
||||
}
|
||||
const auditRefuseCancel = () => { modalAuditRefuseApi.close() }
|
||||
|
||||
const repeat = ref(false)
|
||||
const cashOutAuditFn = (data:any) => {
|
||||
@@ -726,7 +731,7 @@ const memberCancelFn = (data: any) => {
|
||||
*/
|
||||
|
||||
const formRemarkRef = ref<FormInstance>()
|
||||
const remarkShowDialog = ref(false)
|
||||
const [ModalRemark, modalRemarkApi] = useVbenModal()
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
remark: ''
|
||||
@@ -741,7 +746,7 @@ const formRemarkRules = computed(() => {
|
||||
const handleRemark = (data: any) => {
|
||||
formData.id = data.id
|
||||
formData.remark = ''
|
||||
remarkShowDialog.value = true
|
||||
modalRemarkApi.open()
|
||||
}
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
@@ -749,13 +754,14 @@ const save = async (formEl: FormInstance | undefined) => {
|
||||
if (valid) {
|
||||
memberRemark(formData).then((res: any) => {
|
||||
loadOrderList()
|
||||
remarkShowDialog.value = false
|
||||
modalRemarkApi.close()
|
||||
}).catch(() => {
|
||||
remarkShowDialog.value = false
|
||||
modalRemarkApi.close()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const remarkCancel = () => { modalRemarkApi.close() }
|
||||
/**
|
||||
* 会员详情
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</el-table>
|
||||
</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-item :label="t('transferType')">
|
||||
<el-radio-group v-model="transferFormData.refund_type">
|
||||
@@ -42,11 +42,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalRefund>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
@@ -57,6 +57,7 @@ import { t } from '@/lang'
|
||||
import { getPayRefundInfo, getRefundType, getRefundTransfer } from '@/app/api/pay'
|
||||
import { FormInstance } from 'element-plus'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -101,9 +102,9 @@ getRefundType().then((data) => {
|
||||
})
|
||||
})
|
||||
|
||||
const transferDialog = ref(false)
|
||||
const [ModalRefund, modalRefundApi] = useVbenModal()
|
||||
const transferEvent = (data:any) => {
|
||||
transferDialog.value = true
|
||||
modalRefundApi.open()
|
||||
transferFormData.refund_no = data.refund_no
|
||||
transferFormData.refund_money = data.money
|
||||
transferFormData.voucher = ''
|
||||
@@ -136,17 +137,18 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
const data = transferFormData
|
||||
getRefundTransfer(data).then(res => {
|
||||
loading.value = false
|
||||
transferDialog.value = false
|
||||
modalRefundApi.close()
|
||||
refundList.value = []
|
||||
getRefundListInfo(refundNo)
|
||||
emit('loadPayRefundList')
|
||||
}).catch(() => {
|
||||
transferDialog.value = false
|
||||
modalRefundApi.close()
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const refundCancel = () => { modalRefundApi.close() }
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</el-table>
|
||||
</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-item :label="t('transferType')">
|
||||
<el-radio-group v-model="transferFormData.refund_type">
|
||||
@@ -47,11 +47,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalRefund>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,6 +62,7 @@ import { getPayRefundInfo, getRefundType, getRefundTransfer } from '@/app/api/pa
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { FormInstance } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -95,9 +96,9 @@ getRefundType().then((data) => {
|
||||
})
|
||||
})
|
||||
|
||||
const transferDialog = ref(false)
|
||||
const [ModalRefund, modalRefundApi] = useVbenModal()
|
||||
const transferEvent = (data:any) => {
|
||||
transferDialog.value = true
|
||||
modalRefundApi.open()
|
||||
transferFormData.refund_no = data.refund_no
|
||||
transferFormData.refund_money = data.money
|
||||
}
|
||||
@@ -130,16 +131,17 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
const data = transferFormData
|
||||
getRefundTransfer(data).then(res => {
|
||||
loading.value = false
|
||||
transferDialog.value = false
|
||||
modalRefundApi.close()
|
||||
refundList.value = []
|
||||
setFormData(refundNo)
|
||||
}).catch(() => {
|
||||
transferDialog.value = false
|
||||
modalRefundApi.close()
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const refundCancel = () => { modalRefundApi.close() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
</el-empty>
|
||||
</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-card class="box-card !border-none" shadow="never">
|
||||
<el-form-item prop="auth_code">
|
||||
@@ -231,9 +231,9 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</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-item :label="t('title')">
|
||||
<div class="input-width">{{ appStoreInfo.title }}</div>
|
||||
@@ -250,13 +250,13 @@
|
||||
</el-form>
|
||||
<template #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>
|
||||
</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-step :title="t('envCheck')" class="flex-1" />
|
||||
<el-step :title="t('installProgress')" class="flex-1" />
|
||||
@@ -369,8 +369,8 @@
|
||||
</div>
|
||||
<div class="text-[16px] text-[#9699B6] mt-[10px]" v-if="upgradeDuration>0">本次安装用时{{ formatUpgradeDuration }}</div>
|
||||
<div class="mt-[20px]">
|
||||
<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="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">返回</el-button>
|
||||
<el-button @click="installModalFinish" type="primary" class="!w-[90px]">完成</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
@@ -385,13 +385,13 @@
|
||||
{{errorMsg}}
|
||||
</el-scrollbar>
|
||||
<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>
|
||||
</el-result>
|
||||
</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">
|
||||
<div class="min-h-[150px]">
|
||||
<div class="bg-[#fff] my-3" v-if="uninstallCheckResult.dir">
|
||||
@@ -452,18 +452,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-dialog>
|
||||
</ModalUninstallCheck>
|
||||
|
||||
<!-- 下载提示 -->
|
||||
<el-dialog v-model="unloadHintDialog" title="下载提示" width="30%">
|
||||
<ModalUnloadHint :class="'w-[30%]'" title="下载提示">
|
||||
<span>本地已经存在该插件/应用,再次下载会覆盖该插件/应用。</span>
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalUnloadHint>
|
||||
<!-- 更新信息 -->
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -760,6 +760,7 @@ const installAddonFn = (key: string) => {
|
||||
errorDialog.value = false
|
||||
installType.value = ''
|
||||
installShowDialog.value = true
|
||||
installModalApi.open()
|
||||
installAfterTips.value = []
|
||||
installCheckResult.value = res.data
|
||||
userStore.clearRouters()
|
||||
@@ -857,6 +858,7 @@ const formatUpgradeDuration = computed(() => {
|
||||
|
||||
const checkInstallTask = () => {
|
||||
installShowDialog.value = true
|
||||
installModalApi.open()
|
||||
installStep.value = 1
|
||||
}
|
||||
|
||||
@@ -1063,6 +1065,7 @@ const appStoreInfo = ref({})
|
||||
const getAddonDetailFn = (data: any) => {
|
||||
appStoreShowDialog.value = true
|
||||
appStoreInfo.value = data
|
||||
appStoreModalApi.open()
|
||||
}
|
||||
// 更新信息
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="developerDialogVisible" class="developer-dialog-wrap" title="开发人员模式说明" width="30%">
|
||||
<ModalDeveloperTips :class="'w-[30%] developer-dialog-wrap'" title="开发人员模式说明">
|
||||
<div>
|
||||
<p class="text-[16px] mb-[4px]">开发模式</p>
|
||||
<div class="text-[14px] indent-[2em]">开发人员模式即软件开发环境,指框架开启了开发模式(DEBUG=TRUE) ,开发模式时会出现开发选项卡,仅用于开发人员使用,包括应用及插件的安装卸载,系统升级等等。本菜单及子项功能均不受系统管理和权限控制</div>
|
||||
@@ -125,10 +125,10 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="developerDialogVisible = false">确定</el-button>
|
||||
<el-button @click="developerCancel">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalDeveloperTips>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -136,12 +136,13 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
systemStore.setHeadMenu('')
|
||||
|
||||
const router = useRouter()
|
||||
const developerDialogVisible = ref(false)
|
||||
const [ModalDeveloperTips, developerModalApi] = useVbenModal()
|
||||
|
||||
const toLink = (link:any) => {
|
||||
router.push(link)
|
||||
@@ -149,6 +150,7 @@ const toLink = (link:any) => {
|
||||
const goRouter = () => {
|
||||
window.open('https://www.niucloud.com/app')
|
||||
}
|
||||
const developerCancel = () => { developerModalApi.close() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('memberNo')" prop="member_no">
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalAddMember>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -39,6 +39,7 @@ import type { FormInstance } from 'element-plus'
|
||||
import { addMember, getMemberList, getMemberNo } from '@/app/api/member'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
|
||||
const [ModalAddMember, addMemberModalApi] = useVbenModal()
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const repeat = ref(false)
|
||||
@@ -142,6 +143,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
loading.value = false
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
addMemberModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -175,6 +177,9 @@ defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
|
||||
const cancel = () => { showDialog.value = false; addMemberModalApi.close() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('labelName')" prop="label_name">
|
||||
<el-input v-model.trim="formData.label_name" clearable :placeholder="t('labelNamePlaceholder')" class="input-width" />
|
||||
@@ -15,11 +15,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalEditLabel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -29,6 +29,7 @@ import type { FormInstance } from 'element-plus'
|
||||
import { addMemberLabel, updateMemberLabel, getMemberLabelInfo } from '@/app/api/member'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
|
||||
const [ModalEditLabel, editLabelModalApi] = useVbenModal()
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const repeat = ref(false)
|
||||
@@ -92,6 +93,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
loading.value = false
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
editLabelModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -122,6 +124,9 @@ defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
|
||||
const cancel = () => { showDialog.value = false; editLabelModalApi.close() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('headimg')" v-if="type == 'headimg'">
|
||||
@@ -43,12 +43,12 @@
|
||||
|
||||
<template #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-else @click="confirm(formRef)">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalEditMember>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -64,6 +64,7 @@ const type = ref('')
|
||||
const title = ref('')
|
||||
// 会员id
|
||||
const memberId = ref('')
|
||||
const [ModalEditMember, editMemberModalApi] = useVbenModal()
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const repeat = ref(false)
|
||||
@@ -187,6 +188,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
loading.value = false
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
editMemberModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -265,6 +267,7 @@ const batchSetConfirm = async (formEl: FormInstance | undefined) => {
|
||||
loading.value = false
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
editMemberModalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -279,6 +282,9 @@ defineExpose({
|
||||
setDialogType,
|
||||
batchSetDialogType
|
||||
})
|
||||
|
||||
const cancel = () => { showDialog.value = false; editMemberModalApi.close() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('currBalance')" >
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalBalanceEdit>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -37,8 +37,9 @@ import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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 repeat = ref(false)
|
||||
|
||||
@@ -102,7 +103,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
adjustBalance(data).then(res => {
|
||||
loading.value = false
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
modalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -123,12 +124,11 @@ const setFormData = async (row: any = null) => {
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('headimg')">
|
||||
@@ -41,10 +41,10 @@
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalBalanceInfo>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { img } from '@/utils/common'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalBalanceInfo, modalApi] = useVbenModal()
|
||||
const loading = ref(true)
|
||||
|
||||
/**
|
||||
@@ -98,12 +99,11 @@ const setFormData = async (row: any = null) => {
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('headimg')">
|
||||
@@ -41,10 +41,10 @@
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalCommissionInfo>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { img } from '@/utils/common'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalCommissionInfo, modalApi] = useVbenModal()
|
||||
const loading = ref(true)
|
||||
|
||||
/**
|
||||
@@ -98,12 +99,11 @@ const setFormData = async (row: any = null) => {
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('headimg')" >
|
||||
@@ -37,10 +37,10 @@
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalMoneyInfo>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -48,8 +48,9 @@ import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { img } from '@/utils/common'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalMoneyInfo, modalApi] = useVbenModal()
|
||||
const loading = ref(true)
|
||||
|
||||
/**
|
||||
@@ -91,12 +92,11 @@ const setFormData = async (row: any = null) => {
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('currPoint')" >
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
<template #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>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalPointEdit>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -37,8 +37,9 @@ import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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 repeat = ref(false)
|
||||
|
||||
@@ -104,7 +105,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
|
||||
adjustPoint(data).then(res => {
|
||||
loading.value = false
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
modalApi.close()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
@@ -126,12 +127,11 @@ const setFormData = async (row: any = null) => {
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('headimg')" >
|
||||
@@ -41,10 +41,10 @@
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalPointInfo>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -52,8 +52,9 @@ import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { img } from '@/utils/common'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalPointInfo, modalApi] = useVbenModal()
|
||||
const loading = ref(true)
|
||||
|
||||
/**
|
||||
@@ -97,12 +98,11 @@ const setFormData = async (row: any = null) => {
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -6,22 +6,15 @@
|
||||
</el-card>
|
||||
|
||||
<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">
|
||||
<el-form-item :label="t('type')">
|
||||
<el-input v-model.trim="formData.agreement_key_name" readonly class="input-width" />
|
||||
</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>
|
||||
<BaseForm />
|
||||
<el-form-item :label="t('content')">
|
||||
<editor v-model="formModel.content" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<div class="fixed-footer-wrap">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,13 +22,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { getAgreementInfo, editAgreement } from '@/app/api/sys'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useVbenForm } from '@/_env/adapter/form'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -48,67 +41,42 @@ const pageName = route.meta.title
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
agreement_key: '',
|
||||
content: '',
|
||||
title: '',
|
||||
agreement_key_name: ''
|
||||
agreement_key: '',
|
||||
content: '',
|
||||
title: '',
|
||||
agreement_key_name: ''
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
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 = '') => {
|
||||
Object.assign(formData, initialFormData)
|
||||
const data = await (await getAgreementInfo(agreement_key)).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
loading.value = false
|
||||
Object.assign(formModel, initialFormData)
|
||||
const data = await (await getAgreementInfo(agreement_key)).data
|
||||
Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
|
||||
formApi.setModel({ agreement_key_name: formModel.agreement_key_name, title: formModel.title, agreement_key: agreement_key })
|
||||
loading.value = false
|
||||
}
|
||||
if (agreement_key) setFormData(agreement_key)
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const onSave = async () => { if (loading.value) return; formApi.submit() }
|
||||
|
||||
// 表单验证规则
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// editor内容的校验已在 handleSubmit 中做长度校验
|
||||
|
||||
const back = () => {
|
||||
tabbarStore.removeTab(route.path)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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-item :label="t('title')" >
|
||||
@@ -47,18 +47,19 @@
|
||||
|
||||
<template #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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalCronInfo>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const [ModalCronInfo, modalApi] = useVbenModal()
|
||||
const loading = ref(true)
|
||||
|
||||
/**
|
||||
@@ -100,12 +101,11 @@ const setFormData = async (row: any = null) => {
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,51 +1,40 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('messageInfo')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<el-form-item :label="t('messageKey')">
|
||||
<div class="input-width"> {{ formData.name }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('smsType')">
|
||||
<div class="input-width">
|
||||
<div v-if="formData.notice_type == 'sms'">{{ t('sms') }}</div>
|
||||
<div v-if="formData.notice_type == 'wechat'">{{ t('wechat') }}</div>
|
||||
<div v-if="formData.notice_type == 'weapp'">{{ t('weapp') }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item :label="t('messageData')">
|
||||
<div class="input-width"> {{ formData.message_data }} </div>
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item :label="t('nickname')">
|
||||
<div class="input-width"> {{ formData.nickname }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('receiver')">
|
||||
<div class="input-width"> {{ formData.receiver }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('createTime')">
|
||||
<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>
|
||||
<Modal :class="'w-[550px]'" :title="t('messageInfo')">
|
||||
<div class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('messageKey')">
|
||||
<div class="input-width"> {{ formModel.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('smsType')">
|
||||
<div class="input-width">
|
||||
<div v-if="formModel.notice_type == 'sms'">{{ t('sms') }}</div>
|
||||
<div v-if="formModel.notice_type == 'wechat'">{{ t('wechat') }}</div>
|
||||
<div v-if="formModel.notice_type == 'weapp'">{{ t('weapp') }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('nickname')">
|
||||
<div class="input-width"> {{ formModel.nickname }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('receiver')">
|
||||
<div class="input-width"> {{ formModel.receiver }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('createTime')">
|
||||
<div class="input-width"> {{ formModel.create_time }} </div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="close()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -61,34 +50,21 @@ const initialFormData = {
|
||||
receiver: '',
|
||||
notice_type: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
|
||||
}
|
||||
})
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (row) {
|
||||
Object.keys(formModel).forEach((key: string) => { if (row[key] != undefined) formModel[key] = row[key] })
|
||||
}
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const close = () => { modalApi.close() }
|
||||
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,121 +1,82 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_sms">
|
||||
<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('name')">
|
||||
<div class="input-width"> {{ formData.name }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('title')">
|
||||
<div class="input-width"> {{ formData.title }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('smsId')" prop="sms_id">
|
||||
<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>
|
||||
<Modal :class="'w-[550px]'" :title="t('noticeSetting')">
|
||||
<BaseForm />
|
||||
<el-form-item :label="t('name')">
|
||||
<div class="input-width"> {{ formModel.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('title')">
|
||||
<div class="input-width"> {{ formModel.title }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('smsContent')">
|
||||
<div class="input-width"> {{ formModel.content }} </div>
|
||||
</el-form-item>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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 initialFormData = {
|
||||
is_sms: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
sms_default_content: '',
|
||||
title: '',
|
||||
type: '',
|
||||
sms_id: '',
|
||||
content: ''
|
||||
is_sms: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
sms_default_content: '',
|
||||
title: '',
|
||||
type: '',
|
||||
sms_id: '',
|
||||
content: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
sms_id: [
|
||||
{ required: true, message: t('smsIdPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
const data = { ...values, status: values.is_sms }
|
||||
editNotice(data).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
|
||||
},
|
||||
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'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @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 confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
const cancel = () => { modalApi.close() }
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.sms && row.sms[key] != undefined) formData[key] = row.sms[key]
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (row) {
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (row[key] != undefined) formModel[key] = row[key]
|
||||
if (row.sms && row.sms[key] != undefined) formModel[key] = row.sms[key]
|
||||
})
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,45 +1,34 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_weapp">
|
||||
<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('name')">
|
||||
<div class="input-width"> {{ formData.name }} </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>
|
||||
|
||||
<Modal :class="'w-[550px]'" :title="t('noticeSetting')">
|
||||
<BaseForm />
|
||||
<el-form-item :label="t('name')">
|
||||
<div class="input-width"> {{ formModel.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('weappTempKey')">
|
||||
<div class="input-width"> {{ formModel.tid }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('content')">
|
||||
<div class="input-width">
|
||||
<div v-for="(item, index) in formModel.content" :key="index">{{ item[0] }}:{{ item[1] }} </div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<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>
|
||||
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -52,66 +41,45 @@ const initialFormData = {
|
||||
title: '',
|
||||
type: '',
|
||||
content: [],
|
||||
first: '',
|
||||
remark: '',
|
||||
tid: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
|
||||
}
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
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'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @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 confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
const cancel = () => { modalApi.close() }
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
Object.assign(formModel, initialFormData)
|
||||
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.weapp && row.weapp[key] != undefined) formData[key] = row.weapp[key]
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (row[key] != undefined) formModel[key] = row[key]
|
||||
if (row.weapp && row.weapp[key] != undefined) formModel[key] = row.weapp[key]
|
||||
})
|
||||
}
|
||||
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,57 +1,37 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_wechat">
|
||||
<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('name')">
|
||||
<div class="input-width">{{ formData.name }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('tempKey')">
|
||||
<div class="input-width">{{ formData.temp_key }} </div>
|
||||
</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>
|
||||
|
||||
<Modal :class="'w-[550px]'" :title="t('noticeSetting')">
|
||||
<BaseForm />
|
||||
<el-form-item :label="t('name')">
|
||||
<div class="input-width">{{ formModel.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('tempKey')">
|
||||
<div class="input-width">{{ formModel.temp_key }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('keywordNameList')">
|
||||
<div class="input-width">{{ formModel.keyword_name_list ? formModel.keyword_name_list.join(',') : '' }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('content')">
|
||||
<div class="input-width">
|
||||
<div v-for="(item, index) in formModel.content" :key="index">{{ item[0] }}:{{ item[1] }} </div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<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>
|
||||
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -64,69 +44,45 @@ const initialFormData = {
|
||||
title: '',
|
||||
type: '',
|
||||
content: [],
|
||||
// first: '',
|
||||
// remark: '',
|
||||
temp_key: '',
|
||||
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 formRules = computed(() => {
|
||||
return {
|
||||
|
||||
}
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
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'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @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 confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
const cancel = () => { modalApi.close() }
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.wechat && row.wechat[key] != undefined) formData[key] = row.wechat[key]
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (row[key] != undefined) formModel[key] = row[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
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('updateAlipay')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<Modal :class="'w-[550px]'" :title="t('updateAlipay')">
|
||||
<BaseForm />
|
||||
|
||||
<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 />
|
||||
@@ -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-form-item>
|
||||
|
||||
<el-form-item :label="t('appPublicCertPath')" prop="config.app_public_cert_path">
|
||||
<el-form-item :label="t('appPublicCertPath')">
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import Test from '@/utils/test'
|
||||
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 initData = ref<any>(null)
|
||||
/**
|
||||
@@ -65,29 +64,22 @@ const initialFormData = {
|
||||
status: 0,
|
||||
is_default: 0
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
'config.app_id': [
|
||||
{ required: true, message: t('appIdPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.app_secret_cert': [
|
||||
{ required: true, message: t('appSecretCertPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.app_public_cert_path': [
|
||||
{ required: true, message: t('appPublicCertPathPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'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 formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
const merged = { ...values, config: { ...formModel.config } }
|
||||
emit('complete', merged)
|
||||
loading.value = false
|
||||
modalApi.close()
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{ component: 'Input', fieldName: 'config.app_id', label: t('appId'), rules: [{ required: true }] },
|
||||
{ component: 'Textarea', fieldName: 'config.app_secret_cert', label: t('appSecretCert'), rules: [{ required: true }], componentProps: { rows: 4 } }
|
||||
],
|
||||
wrapperClass: 'grid-cols-1'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@@ -96,50 +88,43 @@ const emit = defineEmits(['complete'])
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
|
||||
const cancel = () => {
|
||||
Object.assign(formData, initialFormData)
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (initData.value) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (initData.value[key] != undefined) formData[key] = initData.value[key]
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (initData.value[key] != undefined) formModel[key] = initData.value[key]
|
||||
})
|
||||
formData.channel = initData.value.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
formModel.channel = initData.value.redio_key.split('_')[0]
|
||||
formModel.status = Number(formModel.status)
|
||||
}
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
emit('complete', formModel)
|
||||
modalApi.close()
|
||||
}
|
||||
const setFormData = async (data: any = null) => {
|
||||
initData.value = cloneDeep(data)
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (data[key] != undefined) formModel[key] = data[key]
|
||||
})
|
||||
formData.channel = data.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
formModel.channel = data.redio_key.split('_')[0]
|
||||
formModel.status = Number(formModel.status)
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
const enableVerify = () => {
|
||||
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
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData,
|
||||
enableVerify
|
||||
})
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
<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">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<Modal :class="'w-[550px]'" :title="formModel.config.pay_type_name ? formModel.config.pay_type_name : t('updateFriendsPay')">
|
||||
<BaseForm />
|
||||
<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>
|
||||
<template v-if="formData.config.pay_explain_switch == 1">
|
||||
<el-form-item :label="t('friendsPayTitle')" prop="config.pay_explain_title">
|
||||
<el-input v-model.trim="formData.config.pay_explain_title" :placeholder="t('friendsPayTitlePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
|
||||
<template v-if="formModel.config.pay_explain_switch == 1">
|
||||
<el-form-item :label="t('friendsPayTitle')">
|
||||
<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 :label="t('desContent')" prop="config.pay_explain_content">
|
||||
<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-form-item :label="t('desContent')">
|
||||
<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>
|
||||
</template>
|
||||
<el-form-item :label="t('friendsPayGoodsSwitch')">
|
||||
<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>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('friendsPayName')" prop="config.pay_type_name">
|
||||
<el-input v-model.trim="formData.config.pay_type_name" :placeholder="t('friendsPayNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
|
||||
<el-form-item :label="t('friendsPayName')">
|
||||
<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 :label="t('helpName')" prop="config.pay_page_name">
|
||||
<el-input v-model.trim="formData.config.pay_page_name" :placeholder="t('helpNamePlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
|
||||
<el-form-item :label="t('helpName')">
|
||||
<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 :label="t('helpBtn')" prop="config.pay_button_name">
|
||||
<el-input v-model.trim="formData.config.pay_button_name" :placeholder="t('helpBtnPlaceholder')" class="input-width" maxlength="10" show-word-limit clearable />
|
||||
<el-form-item :label="t('helpBtn')">
|
||||
<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 :label="t('remark')" prop="config.pay_leave_message">
|
||||
<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-form-item :label="t('remark')">
|
||||
<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 :label="t('payWechatImage')" prop="config.pay_wechat_share_image" v-if="initData.redio_key == 'wechat_friendspay'">
|
||||
<upload-image v-model="formData.config.pay_wechat_share_image" :limit="1" />
|
||||
<el-form-item :label="t('payWechatImage')" v-if="initData.redio_key == 'wechat_friendspay'">
|
||||
<upload-image v-model="formModel.config.pay_wechat_share_image" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('payWeappImage')" prop="config.pay_weapp_share_image" v-if="initData.redio_key == 'weapp_friendspay'">
|
||||
<upload-image v-model="formData.config.pay_weapp_share_image" :limit="1" />
|
||||
<el-form-item :label="t('payWeappImage')" v-if="initData.redio_key == 'weapp_friendspay'">
|
||||
<upload-image v-model="formModel.config.pay_weapp_share_image" :limit="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import Test from '@/utils/test'
|
||||
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 initData = ref<any>(null)
|
||||
/**
|
||||
@@ -77,52 +78,23 @@ const initialFormData = {
|
||||
status: 0,
|
||||
is_default: 0
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
'config.pay_explain_title': [
|
||||
{ required: true, message: t('friendsPayTitlePlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (formData.config.pay_explain_switch == 1 && value === '') {
|
||||
callback(new Error(t('friendsPayTitlePlaceholder')))
|
||||
}
|
||||
|
||||
callback()
|
||||
},
|
||||
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 formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
emit('complete', { ...values, config: { ...formModel.config } })
|
||||
loading.value = false
|
||||
modalApi.close()
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{ component: 'Input', fieldName: 'config.pay_type_name', label: t('friendsPayName'), rules: [{ required: true }] },
|
||||
{ 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 }] }
|
||||
],
|
||||
wrapperClass: 'grid-cols-1'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@@ -130,51 +102,40 @@ const emit = defineEmits(['complete'])
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
|
||||
const cancel = () => {
|
||||
Object.assign(formData, initialFormData)
|
||||
if (initData.value) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (initData.value[key] != undefined) formData[key] = initData.value[key]
|
||||
})
|
||||
formData.channel = initData.value.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
}
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (initData.value) {
|
||||
Object.keys(formModel).forEach((key: string) => { if (initData.value[key] != undefined) formModel[key] = initData.value[key] })
|
||||
formModel.channel = initData.value.redio_key.split('_')[0]
|
||||
formModel.status = Number(formModel.status)
|
||||
}
|
||||
emit('complete', formModel)
|
||||
modalApi.close()
|
||||
}
|
||||
|
||||
const setFormData = async (data: any = null) => {
|
||||
initData.value = cloneDeep(data)
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
formData.channel = data.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
}
|
||||
loading.value = false
|
||||
initData.value = cloneDeep(data)
|
||||
loading.value = true
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
|
||||
formModel.channel = data.redio_key.split('_')[0]
|
||||
formModel.status = Number(formModel.status)
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
const enableVerify = () => {
|
||||
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
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData,
|
||||
enableVerify
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('updateOfflinepay')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<Modal :class="'w-[550px]'" :title="t('updateOfflinepay')">
|
||||
<BaseForm />
|
||||
|
||||
<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 />
|
||||
@@ -17,23 +17,22 @@
|
||||
<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-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>
|
||||
<el-button @click="cancel()">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -51,26 +50,24 @@ const initialFormData = {
|
||||
status: 0,
|
||||
is_default: 0
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
'config.collection_name': [
|
||||
{ required: true, message: t('collectionNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.collection_bank': [
|
||||
{ required: true, message: t('collectionBankPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.collection_account': [
|
||||
{ required: true, message: t('collectionAccountPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.collection_desc': [
|
||||
{ required: true, message: t('collectionDescPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
const merged = { ...values }
|
||||
emit('complete', merged)
|
||||
loading.value = false
|
||||
modalApi.close()
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{ component: 'Input', fieldName: 'config.collection_name', label: t('collectionName'), rules: [{ required: true }] },
|
||||
{ component: 'Input', fieldName: 'config.collection_bank', label: t('collectionBank'), rules: [{ required: true }] },
|
||||
{ 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 } }
|
||||
],
|
||||
wrapperClass: 'grid-cols-1'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@@ -79,33 +76,24 @@ const emit = defineEmits(['complete'])
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
|
||||
const setFormData = async (data: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
formData.channel = data.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
}
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formModel).forEach((key: string) => { if (data[key] != undefined) formModel[key] = data[key] })
|
||||
formModel.channel = data.redio_key.split('_')[0]
|
||||
formModel.status = Number(formModel.status)
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('updateWechat')" width="500px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<Modal :class="'w-[500px]'" :title="t('updateWechat')">
|
||||
<BaseForm />
|
||||
|
||||
<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 />
|
||||
@@ -12,33 +12,33 @@
|
||||
<div class="form-tip">{{ t('mchSecretKeyTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('mchSecretCert')" prop="config.mch_secret_cert">
|
||||
<el-form-item :label="t('mchSecretCert')">
|
||||
<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 class="form-tip">{{ t('mchSecretCertTips') }}</div>
|
||||
</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">
|
||||
<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 class="form-tip">{{ t('mchPublicCertPathTips') }}</div>
|
||||
</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">
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
</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">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(wapDomain + '/')">{{ t('copy') }}
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="form-tip !leading-normal">{{ t('jsapiDirTips') }}</div>
|
||||
</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">
|
||||
<template #append>
|
||||
<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>
|
||||
</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">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(serviceDomain)">{{ t('copy') }}
|
||||
@@ -67,27 +67,28 @@
|
||||
</el-input>
|
||||
<div class="form-tip !leading-normal">{{ t('nativeDomainTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { FormInstance, ElMessage } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Test from '@/utils/test'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
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 wapDomain = ref('')
|
||||
const serviceDomain = ref('')
|
||||
@@ -115,26 +116,22 @@ const initialFormData = {
|
||||
status: 0,
|
||||
is_default: 0
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
'config.mch_id': [
|
||||
{ required: true, message: t('mchIdPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.mch_secret_key': [
|
||||
{ required: true, message: t('mchSecretKeyPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.mch_secret_cert': [
|
||||
{ required: true, message: t('mchSecretCertPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
'config.mch_public_cert_path': [
|
||||
{ required: true, message: t('mchPublicCertPathPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
const merged = { ...values, config: { ...formModel.config } }
|
||||
emit('complete', merged)
|
||||
loading.value = false
|
||||
modalApi.close()
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{ component: 'Input', fieldName: 'config.mch_id', label: t('mchId'), rules: [{ required: true }] },
|
||||
{ component: 'Input', fieldName: 'config.mch_secret_key', label: t('mchSecretKey'), rules: [{ required: true }] }
|
||||
],
|
||||
wrapperClass: 'grid-cols-1'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@@ -143,45 +140,39 @@ const emit = defineEmits(['complete'])
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
|
||||
const cancel = () => {
|
||||
Object.assign(formData, initialFormData)
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (initData.value) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (initData.value[key] != undefined) formData[key] = initData.value[key]
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (initData.value[key] != undefined) formModel[key] = initData.value[key]
|
||||
})
|
||||
formData.channel = initData.value.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
formModel.channel = initData.value.redio_key.split('_')[0]
|
||||
formModel.status = Number(formModel.status)
|
||||
}
|
||||
emit('complete', formData)
|
||||
showDialog.value = false
|
||||
emit('complete', formModel)
|
||||
modalApi.close()
|
||||
}
|
||||
const setFormData = async (data: any = null) => {
|
||||
initData.value = cloneDeep(data)
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (data[key] != undefined) formModel[key] = data[key]
|
||||
})
|
||||
formData.channel = data.redio_key.split('_')[0]
|
||||
formData.status = Number(formData.status)
|
||||
formModel.channel = data.redio_key.split('_')[0]
|
||||
formModel.status = Number(formModel.status)
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
const enableVerify = () => {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -210,7 +201,6 @@ watch(copied, () => {
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData,
|
||||
enableVerify
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('aliSms')" width="580px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<Modal :class="'w-[580px]'" :title="t('aliSms')">
|
||||
<BaseForm />
|
||||
<el-form-item :label="t('isUse')">
|
||||
<el-radio-group v-model="formData.is_use">
|
||||
<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-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>
|
||||
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -50,23 +49,21 @@ const initialFormData = {
|
||||
secret_key: '',
|
||||
is_use: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
sign: [
|
||||
{ required: true, message: t('aliSignPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
app_key: [
|
||||
{ required: true, message: t('aliAppKeyPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
secret_key: [
|
||||
{ required: true, message: t('aliSecretKeyPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
editSms(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{ component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
|
||||
{ component: 'Input', fieldName: 'sign', label: t('aliSign'), rules: [{ required: true }] },
|
||||
{ component: 'Input', fieldName: 'app_key', label: t('aliAppKey'), rules: [{ required: true }] },
|
||||
{ component: 'Input', fieldName: 'secret_key', label: t('aliSecretKey'), rules: [{ required: true }] }
|
||||
],
|
||||
wrapperClass: 'grid-cols-1'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@@ -75,44 +72,26 @@ 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 confirm = async () => { if (loading.value) return; formApi.submit() }
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (row) {
|
||||
const data = await (await getSmsInfo(row.sms_type)).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
if (data.params[key] != undefined) formData[key] = data.params[key].value
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (row) {
|
||||
const data = await (await getSmsInfo(row.sms_type)).data
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (data[key] != undefined) formModel[key] = data[key]
|
||||
if (data.params && data.params[key] != undefined) formModel[key] = data.params[key].value
|
||||
})
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const cancel = () => { modalApi.close() }
|
||||
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,51 +1,38 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('messageInfo')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<el-form-item :label="t('messageKey')">
|
||||
<div class="input-width"> {{ formData.name }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('smsType')">
|
||||
<div class="input-width"> {{ formData.sms_type_name }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item :label="t('messageData')">
|
||||
<div class="input-width"> {{ formData.message_data }} </div>
|
||||
</el-form-item> -->
|
||||
|
||||
<!-- <el-form-item :label="t('nickname')">
|
||||
<div class="input-width"> {{ formData.nickname }} </div>
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item :label="t('receiver')">
|
||||
<div class="input-width"> {{ formData.mobile }} </div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('createTime')">
|
||||
<div class="input-width"> {{ formData.create_time }} </div>
|
||||
</el-form-item>
|
||||
<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>
|
||||
<Modal :class="'w-[550px]'" :title="t('messageInfo')">
|
||||
<div class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('messageKey')">
|
||||
<div class="input-width"> {{ formModel.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('smsType')">
|
||||
<div class="input-width"> {{ formModel.sms_type_name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('receiver')">
|
||||
<div class="input-width"> {{ formModel.mobile }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('createTime')">
|
||||
<div class="input-width"> {{ formModel.create_time }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('发送结果')">
|
||||
<div class="input-width" v-if="formModel.status == 'sending'"> 发送失败 </div>
|
||||
<div class="input-width" v-if="formModel.status == 'success'"> 发送成功 </div>
|
||||
<div class="input-width" v-if="formModel.status == 'fail'"> {{ formModel.result }} </div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="close()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -63,34 +50,21 @@ const initialFormData = {
|
||||
status:'',
|
||||
result:''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
|
||||
}
|
||||
})
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (row) {
|
||||
Object.keys(formModel).forEach((key: string) => { if (row[key] != undefined) formModel[key] = row[key] })
|
||||
}
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const close = () => { modalApi.close() }
|
||||
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,52 +1,25 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('tencentSms')" width="580px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<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>
|
||||
|
||||
<Modal :class="'w-[580px]'" :title="t('tencentSms')">
|
||||
<BaseForm />
|
||||
<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>
|
||||
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm()">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
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 initialFormData = {
|
||||
sms_type: '',
|
||||
sign: '',
|
||||
@@ -56,72 +29,44 @@ const initialFormData = {
|
||||
app_id: '',
|
||||
secret_id: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
const formModel: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
sign: [
|
||||
{ required: true, message: t('tencentSignPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
app_id: [
|
||||
{ required: true, message: t('tencentAppIdPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
secret_id: [
|
||||
{ required: true, message: t('tencentSecretIdPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
secret_key: [
|
||||
{ required: true, message: t('tencentSecretKeyPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
commonConfig: { componentProps: { class: 'w-full' } },
|
||||
handleSubmit: async (values) => {
|
||||
loading.value = true
|
||||
editSms(values).then(() => { loading.value = false; modalApi.close(); emit('complete') }).catch(() => { loading.value = false })
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{ component: 'RadioGroup', fieldName: 'is_use', label: t('isUse'), componentProps: { options: [ { label: t('startUsing'), value: 1 }, { label: t('statusDeactivate'), value: 0 } ] } },
|
||||
{ component: 'Input', fieldName: 'sign', label: t('tencentSign'), rules: [{ required: true }] },
|
||||
{ component: 'Input', fieldName: 'app_id', label: t('tencentAppId'), rules: [{ required: true }] },
|
||||
{ component: 'Input', fieldName: 'secret_id', label: t('tencentSecretId'), rules: [{ required: true }] },
|
||||
{ component: 'Input', fieldName: 'secret_key', label: t('tencentSecretKey'), rules: [{ required: true }] }
|
||||
],
|
||||
wrapperClass: 'grid-cols-1'
|
||||
})
|
||||
|
||||
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) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (row) {
|
||||
const data = await (await getSmsInfo(row.sms_type)).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
if (data.params[key] != undefined) formData[key] = data.params[key].value
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
Object.assign(formModel, initialFormData)
|
||||
if (row) {
|
||||
const data = await (await getSmsInfo(row.sms_type)).data
|
||||
Object.keys(formModel).forEach((key: string) => {
|
||||
if (data[key] != undefined) formModel[key] = data[key]
|
||||
if (data.params && data.params[key] != undefined) formModel[key] = data.params[key].value
|
||||
})
|
||||
}
|
||||
formApi.setModel({ ...formModel })
|
||||
loading.value = false
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
const emit = defineEmits(['complete'])
|
||||
const cancel = () => { modalApi.close() }
|
||||
|
||||
defineExpose({ setFormData })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -138,6 +138,12 @@
|
||||
|
||||
</el-form>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -145,6 +151,7 @@
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import { loginAccount, getSmsCaptcha, getSmsSend, resetPassword, registerAccount, getSmsSignConfig } from '@/app/api/notice'
|
||||
import { t } from '@/lang'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const props = defineProps({
|
||||
info: {
|
||||
@@ -501,22 +508,17 @@ const reset = async () => {
|
||||
mobile: changeFormData.value.mobile
|
||||
}
|
||||
resetPassword(changeFormData.value.username, { ...params }).then((res) => {
|
||||
const newPassword = res.data.password
|
||||
ElMessageBox.confirm(`新密码为:${newPassword}`, '请保存好新密码', {
|
||||
confirmButtonText: '确定',
|
||||
showCancelButton: false
|
||||
}).then(() => {
|
||||
type.value = 'login'
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
type.value = 'login'
|
||||
emit('complete')
|
||||
})
|
||||
newPassword.value = res.data.password
|
||||
modalPasswordApi.open()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const [ModalPassword, modalPasswordApi] = useVbenModal()
|
||||
const newPassword = ref('')
|
||||
const passwordConfirm = () => { modalPasswordApi.close(); type.value = 'login'; emit('complete') }
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -26,40 +26,42 @@
|
||||
<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" />
|
||||
</div>
|
||||
<el-dialog v-model="visibleDetail" :title="t('模版详情')" width="600px" destroy-on-close >
|
||||
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('订单编号')" prop="template_id">
|
||||
<div>{{ detail.order_no }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('用户名称')" prop="template_id">
|
||||
<div>{{ detail.username }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('套餐名称')" prop="template_id">
|
||||
<div>{{ detail.package_name }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('订单状态')" prop="title">
|
||||
<div >{{ detail.order_status_name }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('短信条数')" prop="title">
|
||||
<div >{{ detail.sms_num }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('订单金额')" prop="title">
|
||||
<div >{{ detail.order_money }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('付款金额')" prop="title">
|
||||
<div >{{ detail.pay_money }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('创建时间')" prop="title">
|
||||
<div >{{ detail.create_time }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<ModalDetail :class="'w-[600px]'" :title="t('模版详情')">
|
||||
<div v-loading="loading">
|
||||
<el-form label-width="100px" ref="formRef" class="page-form">
|
||||
<el-form-item :label="t('订单编号')" prop="template_id">
|
||||
<div>{{ detail.order_no }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('用户名称')" prop="template_id">
|
||||
<div>{{ detail.username }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('套餐名称')" prop="template_id">
|
||||
<div>{{ detail.package_name }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('订单状态')" prop="title">
|
||||
<div>{{ detail.order_status_name }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('短信条数')" prop="title">
|
||||
<div>{{ detail.sms_num }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('订单金额')" prop="title">
|
||||
<div>{{ detail.order_money }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('付款金额')" prop="title">
|
||||
<div>{{ detail.pay_money }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('创建时间')" prop="title">
|
||||
<div>{{ detail.create_time }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visibleDetail = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="visibleDetail = false">{{ t("confirm") }}</el-button>
|
||||
<el-button @click="detailCancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="detailConfirm">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalDetail>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -67,6 +69,7 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getSmsOrdersList, getOrderInfo } from '@/app/api/notice'
|
||||
import { t } from '@/lang'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const props = defineProps({
|
||||
username: {
|
||||
@@ -106,18 +109,19 @@ const loadRankList = () => {
|
||||
}
|
||||
|
||||
// 详情
|
||||
const [ModalDetail, modalDetailApi] = useVbenModal()
|
||||
const detail = ref({})
|
||||
const visibleDetail = ref(false)
|
||||
const loading = ref(false)
|
||||
const detailEvent = (row:any) => {
|
||||
loading.value = true
|
||||
visibleDetail.value = true
|
||||
modalDetailApi.open()
|
||||
getOrderInfo(props.username, { out_trade_no: row.out_trade_no }).then(res => {
|
||||
detail.value = res.data
|
||||
loading.value = false
|
||||
})
|
||||
visibleDetail.value = true
|
||||
}
|
||||
const detailCancel = () => { modalDetailApi.close() }
|
||||
const detailConfirm = () => { modalDetailApi.close() }
|
||||
onMounted(() => {
|
||||
if (props.username) {
|
||||
loadRankList()
|
||||
|
||||
@@ -81,71 +81,32 @@
|
||||
<el-button @click="visible = false">{{ t("cancel") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="visibleAdd" :title="t('添加签名')" width="800px" destroy-on-close :close-on-click-modal="false">
|
||||
<el-form label-width="150px" :model="formData" ref="formRef" :rules="formRules" class="page-form ml-[20px]">
|
||||
<el-form-item :label="t('短信签名')" prop="signature">
|
||||
<el-input v-model="formData.signature" placeholder="请输入短信签名" class="input-width" maxlength="20" show-word-limit clearable />
|
||||
</el-form-item>
|
||||
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由【】包裹,例如:【test】</div>
|
||||
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符,不能使用空格和特殊符号“ - + = * & % # @ ~等;</div>
|
||||
<el-form-item :label="t('短信示例内容')" prop="contentExample">
|
||||
<el-input v-model="formData.contentExample" placeholder="请输入短信示例内容" clearable maxlength="50" show-word-limit class="input-width" />
|
||||
</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>
|
||||
<Modal :class="'w-[800px]'" :title="t('添加签名')">
|
||||
<BaseForm />
|
||||
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由【】包裹,例如:【test】</div>
|
||||
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符,不能使用空格和特殊符号“ - + = * & % # @ ~等;</div>
|
||||
<el-form-item :label="t('上传图片')">
|
||||
<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>
|
||||
<template #footer>
|
||||
<el-button @click="visibleAdd = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="onSave()">{{ t("confirm") }}</el-button>
|
||||
<el-button @click="modalApi.close()">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="submitVben()">{{ t("confirm") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 { t } from '@/lang'
|
||||
|
||||
const visible = ref(false)
|
||||
const visibleAdd = ref(false)
|
||||
const [Modal, modalApi] = useVbenModal()
|
||||
const emit = defineEmits(['select'])
|
||||
const props = defineProps({
|
||||
username: {
|
||||
@@ -185,6 +146,28 @@ const getSmsSignConfigFn = () => {
|
||||
getSmsSignConfigFn()
|
||||
|
||||
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(() => {
|
||||
return {
|
||||
signature: [
|
||||
@@ -273,18 +256,7 @@ const phoneVerify = (rule: any, value: any, callback: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onSave = async () => {
|
||||
await formRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
addSign(props.username, formData).then((res) => {
|
||||
setTimeout(() => {
|
||||
visibleAdd.value = false
|
||||
loadSignList()
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const submitVben = () => { formApi.submit() }
|
||||
|
||||
// 表单内容
|
||||
const tableData = reactive({
|
||||
@@ -321,7 +293,8 @@ const addEvent = () => {
|
||||
Object.assign(formData, initialFormData)
|
||||
formData.signSource = signConfig.signSourceList[0].type
|
||||
formData.signType = signConfig.signTypeList[0].type
|
||||
visibleAdd.value = true
|
||||
formApi.setModel({ ...formData })
|
||||
modalApi.open()
|
||||
}
|
||||
|
||||
const deleteTemplate = (row:any) => {
|
||||
|
||||
@@ -54,67 +54,66 @@
|
||||
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total"/>
|
||||
</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-item :label="t('短信类型')" prop="template_id">
|
||||
<div>{{ detail.sms_type }}</div>
|
||||
<div>{{ detail?.sms_type }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('模版名称')" prop="template_id">
|
||||
<div>{{ detail.name }}</div>
|
||||
<div>{{ detail?.name }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('模版类型')" prop="title">
|
||||
<div >{{ detail.title }}</div>
|
||||
<div>{{ detail?.title }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('短信内容')" prop="title" v-if="detail.sms">
|
||||
<div >{{ detail.sms?.content }}</div>
|
||||
<el-form-item :label="t('短信内容')" prop="title" v-if="detail?.sms">
|
||||
<div>{{ detail?.sms?.content }}</div>
|
||||
</el-form-item>
|
||||
<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>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<!-- <el-button @click="visibleDetail = false">{{ 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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="visibleReport" :title="t('模版报备')" width="820px" destroy-on-close >
|
||||
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="reportLoading">
|
||||
<el-form-item :label="t('模版名称')" prop="template_id">
|
||||
<div class="input-width">{{ detail.name }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('模版类型')" prop="title">
|
||||
<el-radio-group v-model="reportData.template_type">
|
||||
<el-radio v-for="[key, value] in Object.entries(template_type_list)" :key="key" :label="Number(key)">{{ value }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="ml-[100px] mb-[10px] mt-[-10px] text-[12px] text-[#999] leading-[20px]">
|
||||
<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>
|
||||
</ModalDetail>
|
||||
<ModalReport :class="'w-[820px]'" :title="t('模版报备')">
|
||||
<div v-loading="reportLoading">
|
||||
<el-form label-width="100px" ref="formRef" class="page-form">
|
||||
<el-form-item :label="t('模版名称')" prop="template_id">
|
||||
<div class="input-width">{{ detail?.name }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('模版类型')" prop="title">
|
||||
<el-radio-group v-model="reportData.template_type">
|
||||
<el-radio v-for="[key, value] in Object.entries(template_type_list)" :key="key" :label="Number(key)">{{ value }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="ml-[100px] mb-[10px] mt-[-10px] text-[12px] text-[#999] leading-[20px]">
|
||||
<div>验证码:仅支持验证码类型变量</div>
|
||||
<div>行业通知:不支持验证码类型变量</div>
|
||||
<div>营销推广:不支持变量</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
<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>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visibleReport = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="reportTemplateFn()" :disabled="isMarketingWithVariable">{{ t("confirm") }}</el-button>
|
||||
<el-button @click="reportCancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="reportConfirm" :disabled="isMarketingWithVariable">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="visibleAsync" :title="t('同步模版状态')" width="800px" destroy-on-close >
|
||||
</ModalReport>
|
||||
<ModalAsync :class="'w-[800px]'" :title="t('同步模版状态')">
|
||||
<el-alert type="warning" :closable="false" class="!mb-[10px]">
|
||||
<template #default>
|
||||
以下模版名称重复,请先调整模版名称后重新同步模版
|
||||
@@ -125,20 +124,20 @@
|
||||
<el-table :data="repeatListArray" border style="width: 100%;">
|
||||
<el-table-column label="模版名称" prop="name" />
|
||||
<el-table-column label="插件名称">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="item in row.platforms" :key="item" class="mr-1 mb-1">{{ item }}</el-tag>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="item in row.platforms" :key="item" class="mr-1 mb-1">{{ item }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visibleAsync = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="visibleAsync = false">{{ t("confirm") }}</el-button>
|
||||
<el-button @click="asyncCancel">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="asyncConfirm">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ModalAsync>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -146,6 +145,7 @@
|
||||
import { ref, computed, reactive, onMounted, watch } from 'vue'
|
||||
import { getTemplateList, getTemplateReportConfig, reportTemplate, templateSync, getreportTemplateInfo, clearTemplate } from '@/app/api/notice'
|
||||
import { t } from '@/lang'
|
||||
import { useVbenModal } from '@vben/common-ui'
|
||||
|
||||
const props = defineProps({
|
||||
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 syncEvent = () => {
|
||||
templateSync('niuyun', props.username).then((res) => {
|
||||
repeatList.value = res.data.repeat_list
|
||||
if (repeatList.value && Object.keys(repeatList.value).length > 0) {
|
||||
visibleAsync.value = true
|
||||
modalAsyncApi.open()
|
||||
} else {
|
||||
loadSmsTemplateList()
|
||||
}
|
||||
@@ -244,10 +246,9 @@ const repeatListArray = computed(() => {
|
||||
|
||||
// 详情
|
||||
const detail = ref(null)
|
||||
const visibleDetail = ref(false)
|
||||
const editEvent = (row:any) => {
|
||||
visibleDetail.value = true
|
||||
detail.value = row
|
||||
modalDetailApi.open()
|
||||
}
|
||||
|
||||
// 清除报备
|
||||
@@ -267,7 +268,6 @@ const clearEvent = (row:any) => {
|
||||
const template_params_type_list = ref({})
|
||||
const template_type_list = ref({})
|
||||
const template_status_list = ref({})
|
||||
const visibleReport = ref(false)
|
||||
const reportData = ref({
|
||||
template_type: 1,
|
||||
template_key: '',
|
||||
@@ -307,9 +307,9 @@ const reportEvent = (row:any) => {
|
||||
if (!signature) {
|
||||
ElMessage.error('请先配置签名')
|
||||
} else {
|
||||
detail.value = row
|
||||
modalReportApi.open()
|
||||
if (row.template_id) {
|
||||
visibleReport.value = true
|
||||
detail.value = row
|
||||
getreportTemplateInfo('niuyun', props.username, { template_key: row.key }).then((res) => {
|
||||
const paramJson = res.data?.param_json ?? {}
|
||||
reportData.value.template_key = res.data.template_key
|
||||
@@ -323,9 +323,7 @@ const reportEvent = (row:any) => {
|
||||
reportLoading.value = false
|
||||
})
|
||||
} else {
|
||||
visibleReport.value = true
|
||||
reportLoading.value = false
|
||||
detail.value = row
|
||||
reportData.value.template_type = 1
|
||||
reportData.value.template_key = detail.value.key
|
||||
reportData.value.params_json = {}
|
||||
@@ -350,10 +348,16 @@ const reportTemplateFn = () => {
|
||||
reportData.value.template_id = Number(detail.value.template_id)
|
||||
}
|
||||
reportTemplate(detail.value.sms_type, props.username, reportData.value).then((res) => {
|
||||
visibleReport.value = false
|
||||
modalReportApi.close()
|
||||
loadSmsTemplateList()
|
||||
})
|
||||
}
|
||||
|
||||
const detailConfirm = () => { modalDetailApi.close() }
|
||||
const reportCancel = () => { modalReportApi.close() }
|
||||
const reportConfirm = () => { reportTemplateFn() }
|
||||
const asyncCancel = () => { modalAsyncApi.close() }
|
||||
const asyncConfirm = () => { modalAsyncApi.close() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user