mirror of
https://gitee.com/likeadmin/likeadmin_java.git
synced 2026-06-11 16:54:40 +08:00
Compare commits
276 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cf3be989d | ||
|
|
af661249e6 | ||
|
|
ac3680fda8 | ||
|
|
add675f5c6 | ||
|
|
7c31ed57b4 | ||
|
|
7a6e2a755f | ||
|
|
3ea235351d | ||
|
|
acd1f22dfc | ||
|
|
fe9e4557ab | ||
|
|
6010b19826 | ||
|
|
b9f40163ab | ||
|
|
0ba266804b | ||
|
|
36bb1f7b7d | ||
|
|
e88f113183 | ||
|
|
c153122e99 | ||
|
|
b50df74802 | ||
|
|
2b890b65f3 | ||
|
|
f353f78a04 | ||
|
|
31f6d5293b | ||
|
|
c96b3c210b | ||
|
|
d96bd7c3e5 | ||
|
|
bb344261c3 | ||
|
|
7873cdf216 | ||
|
|
b0a9b4570e | ||
|
|
adc909db55 | ||
|
|
ca93adfcaf | ||
|
|
923c2b1d72 | ||
|
|
3fec3b5922 | ||
|
|
a1e15274e0 | ||
|
|
e470a714b9 | ||
|
|
3a379a2bf4 | ||
|
|
eb266d6877 | ||
|
|
12db298225 | ||
|
|
d2ff42dd32 | ||
|
|
05af28cf4d | ||
|
|
3fcd297979 | ||
|
|
4f723d5237 | ||
|
|
89598da689 | ||
|
|
9267f497c2 | ||
|
|
e889caf07c | ||
|
|
5e5c99dfcd | ||
|
|
25805ce7dc | ||
|
|
5cefba25c2 | ||
|
|
4ba5f5b1da | ||
|
|
ffce43ba3e | ||
|
|
843228d047 | ||
|
|
cfa05f6e08 | ||
|
|
409e50befd | ||
|
|
8004ee15ea | ||
|
|
6a977e75e4 | ||
|
|
2cb2bd3cb1 | ||
|
|
649b7f9bcb | ||
|
|
fd500bbd0d | ||
|
|
a20982eabc | ||
|
|
3ca0777ea4 | ||
|
|
795b3da57d | ||
|
|
54184be9c5 | ||
|
|
8f9b2fc261 | ||
|
|
af927ba3ce | ||
|
|
252f9cf723 | ||
|
|
a647071d8e | ||
|
|
a1bd8d56be | ||
|
|
4ad7a7b785 | ||
|
|
66e4f40bb3 | ||
|
|
e3264ea20e | ||
|
|
3192c9e193 | ||
|
|
8affe5c501 | ||
|
|
26c9f4d27f | ||
|
|
e003d98100 | ||
|
|
93d55c0e22 | ||
|
|
4617c65581 | ||
|
|
e6a92a479b | ||
|
|
07c1dd3a72 | ||
|
|
14b896eee0 | ||
|
|
17d527d60a | ||
|
|
e00563d3b5 | ||
|
|
23e8bc4c91 | ||
|
|
795a62bf1d | ||
|
|
c1de717eaa | ||
|
|
a2982000a6 | ||
|
|
4d27c8c84d | ||
|
|
80e9a04c14 | ||
|
|
f0d35527d9 | ||
|
|
8475dc5324 | ||
|
|
a9549e23c9 | ||
|
|
57053cc0e6 | ||
|
|
f95e28a2d8 | ||
|
|
9a6f8128ac | ||
|
|
d8c4799028 | ||
|
|
0b3c9ff267 | ||
|
|
5f20a09b93 | ||
|
|
1ac24fbde6 | ||
|
|
7ca5d1d274 | ||
|
|
426e6d3225 | ||
|
|
3a6b81ec61 | ||
|
|
e5fe98cc54 | ||
|
|
8fee113169 | ||
|
|
708931995f | ||
|
|
b0bb903643 | ||
|
|
2f854d5a94 | ||
|
|
9deb398f92 | ||
|
|
ba6c0a2849 | ||
|
|
6d52d34e41 | ||
|
|
1088d305c5 | ||
|
|
42928372f6 | ||
|
|
35e8ed52e6 | ||
|
|
f5ba7d86f1 | ||
|
|
62e4e9d388 | ||
|
|
ad8063f15b | ||
|
|
18cf595f7f | ||
|
|
38419d2524 | ||
|
|
f204e0d54b | ||
|
|
763f340709 | ||
|
|
694e1ee37f | ||
|
|
55436c011a | ||
|
|
76ba23188c | ||
|
|
d5c6226930 | ||
|
|
a32d023a8c | ||
|
|
b3b57c13d1 | ||
|
|
3fc536eb1c | ||
|
|
3456eb65d4 | ||
|
|
c3ede05af0 | ||
|
|
a8025c2f24 | ||
|
|
07bed0ab0d | ||
|
|
9c9db1c79b | ||
|
|
81bdb119d2 | ||
|
|
d1b1317d1f | ||
|
|
aca9f8fc62 | ||
|
|
d9f17eabab | ||
|
|
47e14dae53 | ||
|
|
65f7d0fd52 | ||
|
|
d6922e31dc | ||
|
|
7fac900ba1 | ||
|
|
a3aafaf552 | ||
|
|
fd8e0d5fc8 | ||
|
|
05f21a3168 | ||
|
|
0c0859ac5b | ||
|
|
3ace0ac688 | ||
|
|
6ecf919440 | ||
|
|
0ea4373167 | ||
|
|
9f31b9ce3d | ||
|
|
e9b882dd9b | ||
|
|
787e8edc45 | ||
|
|
638d5865dc | ||
|
|
daf6bee8d7 | ||
|
|
3e2dd74717 | ||
|
|
80bcf29910 | ||
|
|
8a0e675549 | ||
|
|
1395beb9d2 | ||
|
|
356d83bc91 | ||
|
|
d3b5794ce7 | ||
|
|
d66570d3bb | ||
|
|
e9bacd433d | ||
|
|
2d74edc701 | ||
|
|
38a6de13a5 | ||
|
|
b95e43c7ac | ||
|
|
f1c9a66127 | ||
|
|
294ddbf4bc | ||
|
|
06e219d2a1 | ||
|
|
710f20f093 | ||
|
|
5eb9a04d6d | ||
|
|
74481785fd | ||
|
|
41b4024fcf | ||
|
|
2166e40352 | ||
|
|
3837d78fce | ||
|
|
d562ddde48 | ||
|
|
79bdca1412 | ||
|
|
decbccedce | ||
|
|
1b56ede827 | ||
|
|
65d10f608c | ||
|
|
932d46c6ee | ||
|
|
3cb5582b20 | ||
|
|
d5d397b2e9 | ||
|
|
36e4ea6cf1 | ||
|
|
4bc1e28685 | ||
|
|
58999e6fbc | ||
|
|
c84b597032 | ||
|
|
daf6ba26a4 | ||
|
|
8c19fc0cb2 | ||
|
|
75eb7f42f3 | ||
|
|
7bc8d4fce1 | ||
|
|
eff789eb82 | ||
|
|
6cadf25368 | ||
|
|
532653107b | ||
|
|
9a9ae9f336 | ||
|
|
b63d93364a | ||
|
|
da29c32357 | ||
|
|
a636a36884 | ||
|
|
e2ec0d6c9a | ||
|
|
268f46b872 | ||
|
|
1d0d314aee | ||
|
|
44975bfd86 | ||
|
|
3a6f6bef97 | ||
|
|
546889cf6f | ||
|
|
1c480b9630 | ||
|
|
9cc41d2e80 | ||
|
|
970abf6a47 | ||
|
|
d6136463bb | ||
|
|
64815f2f4f | ||
|
|
bacda99c59 | ||
|
|
8718c22cee | ||
|
|
c10464d873 | ||
|
|
f6589a8e33 | ||
|
|
9223b88a72 | ||
|
|
6cdc2b82e1 | ||
|
|
84cded01f2 | ||
|
|
f2038fa7af | ||
|
|
ecd5abbd05 | ||
|
|
8481c63ef2 | ||
|
|
20b7c760fd | ||
|
|
9595769d46 | ||
|
|
9e5bd06e37 | ||
|
|
f51fab1deb | ||
|
|
70fb9d4b9b | ||
|
|
a8559f0d70 | ||
|
|
b2bfaca816 | ||
|
|
f2bbfffe10 | ||
|
|
803a50cf3c | ||
|
|
c46903a210 | ||
|
|
2f161aa3fe | ||
|
|
22e7aa77cd | ||
|
|
18360d43e5 | ||
|
|
b360bb7485 | ||
|
|
ded1cabf1e | ||
|
|
f4b6179336 | ||
|
|
35b485c0a7 | ||
|
|
f39417f9b9 | ||
|
|
313121b2ab | ||
|
|
a6ad0a2feb | ||
|
|
e20fe09db4 | ||
|
|
bf31eff2e0 | ||
|
|
8b450e991f | ||
|
|
86c8697a0f | ||
|
|
16fbaa8b4e | ||
|
|
0f08a3ec04 | ||
|
|
54fd266103 | ||
|
|
b4dadbee38 | ||
|
|
45645668ef | ||
|
|
79a3959a17 | ||
|
|
678c5a5fb6 | ||
|
|
b081a2b3e5 | ||
|
|
21866eb315 | ||
|
|
5880ea48af | ||
|
|
ae398bf417 | ||
|
|
96ce0b284c | ||
|
|
5d056280b0 | ||
|
|
712538213b | ||
|
|
97518d66d3 | ||
|
|
570198d9bc | ||
|
|
f48116e8d1 | ||
|
|
d6bd188203 | ||
|
|
144fe94ecb | ||
|
|
667b92265f | ||
|
|
964e9c5bd7 | ||
|
|
0daebef757 | ||
|
|
691786b478 | ||
|
|
93aafd2dbe | ||
|
|
8f936f2df4 | ||
|
|
27bcde0da0 | ||
|
|
1f193e7850 | ||
|
|
55ee7d8a47 | ||
|
|
c45df35bd9 | ||
|
|
853e64fee5 | ||
|
|
f2450bc615 | ||
|
|
ed6b24c677 | ||
|
|
7a71bcf238 | ||
|
|
22bebdf804 | ||
|
|
6f8e8442c0 | ||
|
|
b693e829f9 | ||
|
|
8d37e36395 | ||
|
|
7419ceb502 | ||
|
|
f6491d1ba0 | ||
|
|
84c4b305d1 | ||
|
|
5f3260c586 | ||
|
|
c5781cd796 | ||
|
|
9d4964af1b |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,4 +7,6 @@ application-dev.yml
|
||||
application-pro.yml
|
||||
application-dem.yml
|
||||
application-prod.yml
|
||||
application-test.yml
|
||||
application-test.yml
|
||||
/public/uploads/*
|
||||
!/public/uploads/index.html
|
||||
35
README.md
35
README.md
@@ -1,7 +1,18 @@
|
||||
<h1 align="center">likeadmin通用管理后台(Java)</h1>
|
||||
<h4 align="center">🚀快速开发、🛠️ 一键生成代码、✅后台多端自适应、📱手机端uniapp前台</h4>
|
||||
<h4 align="center">⚡️快速开发、🛠️ 一键生成代码、✅后台多端自适应、📱手机端、🖥️PC(电脑)端前台</h4>
|
||||
<p align="center">
|
||||
<a href="https://www.java.com/zh-CN/download/"><img src="https://img.shields.io/badge/JAVA-8-d74f11"> </a><a href="#"> <img src="https://img.shields.io/badge/Spring Boot-2-5ea931"> </a><a href="https://www.tslang.cn/"><img src="https://img.shields.io/badge/TypeScript-3-294e80"></a> <a href="#"><img src="https://img.shields.io/badge/Vue.js-3-4eb883"> </a><a href="#"><img src="https://img.shields.io/badge/vite-2-ffc018"> </a><a href="#"><img src="https://img.shields.io/badge/Element Plus-2-409eff"> </a><a target="_blank" href="https://www.docker.com/"><img src="https://img.shields.io/badge/Docker--139cff"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://mp.weixin.qq.com/"><img src="https://img.shields.io/badge/微信-公众号-05ce66"></a>
|
||||
<a href="https://mp.weixin.qq.com/"><img src="https://img.shields.io/badge/微信-小程序-05ce66"></a>
|
||||
<a href="https://open.weixin.qq.com/"><img src="https://img.shields.io/badge/微信-开放平台-05ce66"></a>
|
||||
<a href="https://cloud.tencent.com/"><img src="https://img.shields.io/badge/腾讯云-COS-00a3ff"></a>
|
||||
<a href="https://cloud.tencent.com/"><img src="https://img.shields.io/badge/腾讯云-短信-00a3ff"></a>
|
||||
<a href="https://www.aliyun.com/"><img src="https://img.shields.io/badge/阿里云-OSS-ff6a00"></a>
|
||||
<a href="https://www.aliyun.com/"><img src="https://img.shields.io/badge/阿里云-短信-ff6a00"></a>
|
||||
<a href="https://www.qiniu.com/"><img src="https://img.shields.io/badge/七牛云-OSS-07beff"></a>
|
||||
</p>
|
||||
<div align="center">
|
||||
<img width="80%" src="https://md.likeshop.cn/server/index.php?s=/api/attachment/visitFile&sign=9cf02b831e49d6a411bafbc4d79f51d4" /><br>
|
||||
</div>
|
||||
@@ -10,10 +21,14 @@
|
||||
|
||||
## 👀体验
|
||||
### 管理后台
|
||||
地址:https://demo-java.likeadmin.cn <br>
|
||||
地址:https://java-admin.likeadmin.cn <br>
|
||||
账号:admin 密码:123456
|
||||
### 手机端uniapp前台
|
||||
<img width="40%" src="https://md.likeshop.cn/server/index.php?s=/api/attachment/visitFile&sign=260c0869d9ba7e692b2db1e216078241" /><br>
|
||||
### 手机端网页前台
|
||||
https://java-front.likeadmin.cn/mobile
|
||||
### PC(电脑)端网页前台
|
||||
https://java-front.likeadmin.cn/pc/
|
||||
### 开发文档
|
||||
地址:[https://www.likeadmin.cn](https://www.likeadmin.cn "https://www.likeadmin.cn")
|
||||
|
||||
@@ -23,15 +38,15 @@
|
||||
<br>
|
||||
## 🧐进一步了解
|
||||
### 🧰场景介绍
|
||||
1.likeadmin已经搭建好前后端分离的底层,包含程序安装、登录、登出、工作台、菜单权限控制、角色、管理员、部门管理、岗位管理、素材管理、网站设置、图库管理等基础功能,无需重复造轮子。更有开发者工具功能,一生成代码,大大节省开发时间。<br>
|
||||
1.likeadmin已经搭建好前后端分离的底层,包含程序安装、登录、登出、工作台、菜单权限控制、角色、管理员、部门管理、岗位管理、素材管理、网站设置、图库管理等基础功能,无需重复造轮子。更有开发者工具功能,一键生成代码,大大节省开发时间。<br>
|
||||
2.可视化系统程序安装界面,可自定义安装数据,开发者可快速扩展发行自己的软件产品。<br>
|
||||
3.likeadmin定位为通用的软件系统管理后台,方便开发者快速开发软件系统,文档清晰、代码易懂、简单易用。<br>
|
||||
4.手机端uniapp前台,含导导航配置、微信登录、个人登录等等基础功能,方便根据业务开发含手机前台的项目。
|
||||
|
||||
|
||||
### 🐙 后端架构方面
|
||||
1.服务端使用JAVA8开发,性能有突破性的提升。<br>
|
||||
2.使用Spring Boot2.5框架,目前国内流行的JAVA框架,高性能、简单易用、文档齐全、支持Mave高级项目管理工具、支持Redis等。
|
||||
1.服务端使用Java8开发,性能有突破性的提升。<br>
|
||||
2.使用Spring Boot2.7.5框架,目前国内流行的Java框架,高性能、简单易用、文档齐全、支持Mave高级项目管理工具、支持Redis等。
|
||||
### 🐹 前端架构方面
|
||||
#### 后台
|
||||
1.使用最流性的前后端分离方案typescript、vue3、vite开发,保持了代码的简洁、一致和规范。<br>
|
||||
@@ -43,11 +58,11 @@
|
||||
一键生成前后端业务代码,大大提示开发效率。<br>
|
||||
## 界面预览
|
||||
### 🖥️后台页面
|
||||
<br>
|
||||

|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
 <br>
|
||||
 <br>
|
||||
 <br>
|
||||
 <br>
|
||||
 <br>
|
||||
<br>
|
||||
### 📱手机端前台界面
|
||||
<div class="half">
|
||||
|
||||
10
admin/src/api/app/recharge.ts
Normal file
10
admin/src/api/app/recharge.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getRechargeConfig() {
|
||||
return request.get({ url: '/marketing/recharge/detail' })
|
||||
}
|
||||
|
||||
// 设置
|
||||
export function setRechargeConfig(params: any) {
|
||||
return request.post({ url: '/marketing/recharge/save', params })
|
||||
}
|
||||
@@ -2,10 +2,10 @@ import request from '@/utils/request'
|
||||
|
||||
// 微信开发平台配置保存
|
||||
export function setWxDevConfig(params: any) {
|
||||
return request.post({ url: '/channel/wx/save', params })
|
||||
return request.post({ url: '/channel/op/save', params })
|
||||
}
|
||||
|
||||
// 微信开发平台配置详情
|
||||
export function getWxDevConfig() {
|
||||
return request.get({ url: '/channel/wx/detail' })
|
||||
return request.get({ url: '/channel/op/detail' })
|
||||
}
|
||||
31
admin/src/api/finance.ts
Normal file
31
admin/src/api/finance.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 余额明细
|
||||
export function accountLog(params?: any) {
|
||||
return request.get({ url: '/finance/wallet/list', params })
|
||||
}
|
||||
|
||||
// 充值记录
|
||||
export function rechargeLists(params?: any) {
|
||||
return request.get({ url: '/finance/recharger/list', params }, { ignoreCancelToken: true })
|
||||
}
|
||||
|
||||
//退款
|
||||
export function refund(params?: any) {
|
||||
return request.post({ url: '/finance/recharger/refund', params })
|
||||
}
|
||||
|
||||
//重新退款
|
||||
export function refundAgain(params?: any) {
|
||||
return request.post({ url: '/finance/recharger/refundAgain', params })
|
||||
}
|
||||
|
||||
//退款记录
|
||||
export function refundRecord(params?: any) {
|
||||
return request.get({ url: '/finance/refund/list', params })
|
||||
}
|
||||
|
||||
//退款日志
|
||||
export function refundLog(params?: any) {
|
||||
return request.get({ url: '/finance/refund/log', params })
|
||||
}
|
||||
26
admin/src/api/setting/pay.ts
Normal file
26
admin/src/api/setting/pay.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取支付方式
|
||||
export function getPayWay() {
|
||||
return request.get({ url: '/setting/payment/method' })
|
||||
}
|
||||
|
||||
// 设置支付方式
|
||||
export function setPayWay(params: any) {
|
||||
return request.post({ url: '/setting/payment/editMethod', params })
|
||||
}
|
||||
|
||||
// 获取支付方式
|
||||
export function getPayConfigLists() {
|
||||
return request.get({ url: '/setting/payment/list' })
|
||||
}
|
||||
|
||||
// 设置支付方式
|
||||
export function setPayConfig(params: any) {
|
||||
return request.post({ url: '/setting/payment/editConfig', params })
|
||||
}
|
||||
|
||||
// 设置支付方式
|
||||
export function getPayConfig(params: any) {
|
||||
return request.get({ url: '/setting/payment/detail', params })
|
||||
}
|
||||
@@ -39,3 +39,8 @@ export function crontabEdit(params: any) {
|
||||
export function crontabDel(params: any) {
|
||||
return request.post({ url: '/crontab/del', params })
|
||||
}
|
||||
|
||||
// 获取登录日志列表
|
||||
export function loginLogLists(params: any) {
|
||||
return request.get({ url: '/system/log/login', params })
|
||||
}
|
||||
|
||||
@@ -10,11 +10,15 @@ export function dataTable(params: any) {
|
||||
return request.get({ url: '/gen/db', params })
|
||||
}
|
||||
|
||||
// 数据表列表接口
|
||||
// 数据表所有列表接口
|
||||
export function dataTableAll() {
|
||||
return request.get({ url: '/gen/dbAll' })
|
||||
}
|
||||
|
||||
//表名查字段
|
||||
export function dataTableToColumn(params: any) {
|
||||
return request.get({ url: '/gen/dbColumn', params })
|
||||
}
|
||||
//选择要生成代码的数据表
|
||||
export function selectTable(params: any) {
|
||||
return request.post(
|
||||
|
||||
@@ -6,6 +6,11 @@ export function login(params: Record<string, any>) {
|
||||
return request.post({ url: '/system/login', params: { ...params, terminal: config.terminal } })
|
||||
}
|
||||
|
||||
// 登录
|
||||
export function loginCaptcha() {
|
||||
return request.get({ url: '/system/captcha' })
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
export function logout() {
|
||||
return request.post({ url: '/system/logout' })
|
||||
|
||||
@@ -92,6 +92,11 @@ const linkList = ref([
|
||||
path: '/pages/search/search',
|
||||
name: '搜索',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
},
|
||||
{
|
||||
path: '/packages/pages/user_wallet/user_wallet',
|
||||
name: '我的钱包',
|
||||
type: LinkTypeEnum.SHOP_PAGES
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const config = {
|
||||
terminal: 1, //终端
|
||||
title: '后台管理系统', //网站默认标题
|
||||
version: '1.3.3', //版本号
|
||||
version: '1.4.0', //版本号
|
||||
baseUrl: `${import.meta.env.VITE_APP_BASE_URL || ''}/`, //请求接口域名
|
||||
urlPrefix: 'api', //请求默认前缀
|
||||
timeout: 10 * 1000 //请求超时时长
|
||||
|
||||
@@ -22,6 +22,7 @@ export enum RequestCodeEnum {
|
||||
LOGIN_DISABLE_ERROR = 331, //登陆账号已被禁用
|
||||
TOKEN_EMPTY = 332, // TOKEN参数为空
|
||||
TOKEN_INVALID = 333, // TOKEN参数无效
|
||||
VERIFICATION_CODE_ERROR = 334, // 验证码错误
|
||||
NO_PERMISSTION = 403, //无相关权限
|
||||
REQUEST_404_ERROR = 404, //请求接口不存在
|
||||
SYSTEM_ERROR = 500 //系统错误
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFunction } from 'lodash'
|
||||
import { reactive, toRaw } from 'vue'
|
||||
|
||||
// 分页钩子函数
|
||||
@@ -7,10 +8,20 @@ interface Options {
|
||||
fetchFun: (_arg: any) => Promise<any>
|
||||
params?: Record<any, any>
|
||||
firstLoading?: boolean
|
||||
beforeRequest?(params: Record<any, any>): Record<any, any>
|
||||
afterRequest?(res: Record<any, any>): void
|
||||
}
|
||||
|
||||
export function usePaging(options: Options) {
|
||||
const { page = 1, size = 15, fetchFun, params = {}, firstLoading = false } = options
|
||||
const {
|
||||
page = 1,
|
||||
size = 15,
|
||||
fetchFun,
|
||||
params = {},
|
||||
firstLoading = false,
|
||||
beforeRequest,
|
||||
afterRequest
|
||||
} = options
|
||||
// 记录分页初始参数
|
||||
const paramsInit: Record<any, any> = Object.assign({}, toRaw(params))
|
||||
// 分页数据
|
||||
@@ -19,19 +30,28 @@ export function usePaging(options: Options) {
|
||||
size,
|
||||
loading: firstLoading,
|
||||
count: 0,
|
||||
lists: [] as any[]
|
||||
lists: [] as any[],
|
||||
extend: {} as Record<any, any>
|
||||
})
|
||||
// 请求分页接口
|
||||
const getLists = () => {
|
||||
pager.loading = true
|
||||
let requestParams = params
|
||||
if (isFunction(beforeRequest)) {
|
||||
requestParams = beforeRequest(params)
|
||||
}
|
||||
return fetchFun({
|
||||
pageNo: pager.page,
|
||||
pageSize: pager.size,
|
||||
...params
|
||||
...requestParams
|
||||
})
|
||||
.then((res: any) => {
|
||||
pager.count = res?.count
|
||||
pager.lists = res?.lists
|
||||
pager.extend = res?.extend
|
||||
if (isFunction(afterRequest)) {
|
||||
afterRequest(res)
|
||||
}
|
||||
return Promise.resolve(res)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
|
||||
@@ -6,5 +6,6 @@ import './styles/index.scss'
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
const app = createApp(App)
|
||||
console.log(app)
|
||||
app.use(install)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -34,11 +34,13 @@ const useUserStore = defineStore({
|
||||
this.perms = []
|
||||
},
|
||||
login(playload: any) {
|
||||
const { account, password } = playload
|
||||
const { account, password, code, uuid } = playload
|
||||
return new Promise((resolve, reject) => {
|
||||
login({
|
||||
username: account,
|
||||
password: password
|
||||
password,
|
||||
code,
|
||||
uuid
|
||||
})
|
||||
.then((data) => {
|
||||
this.token = data.token
|
||||
|
||||
@@ -69,6 +69,7 @@ const axiosHooks: AxiosHooks = {
|
||||
case RequestCodeEnum.NO_PERMISSTION:
|
||||
case RequestCodeEnum.FAILED:
|
||||
case RequestCodeEnum.SYSTEM_ERROR:
|
||||
case RequestCodeEnum.VERIFICATION_CODE_ERROR:
|
||||
msg && feedback.msgError(msg)
|
||||
return Promise.reject(data)
|
||||
|
||||
|
||||
@@ -34,6 +34,25 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<div class="flex items-center">
|
||||
<el-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prepend>
|
||||
<icon name="local-icon-anquan" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div
|
||||
class="ml-4 w-[100px] flex-none cursor-pointer"
|
||||
@click="getLoginCaptcha"
|
||||
>
|
||||
<img class="w-full" :src="codeImg" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="mb-5">
|
||||
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
||||
@@ -58,6 +77,7 @@ import cache from '@/utils/cache'
|
||||
import { ACCOUNT_KEY } from '@/enums/cacheEnums'
|
||||
import { PageEnum } from '@/enums/pageEnum'
|
||||
import { useLockFn } from '@/hooks/useLockFn'
|
||||
import { loginCaptcha } from '@/api/user'
|
||||
const passwordRef = shallowRef<InputInstance>()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const appStore = useAppStore()
|
||||
@@ -66,9 +86,12 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const remAccount = ref(false)
|
||||
const config = computed(() => appStore.config)
|
||||
const codeImg = ref()
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
password: ''
|
||||
password: '',
|
||||
code: '',
|
||||
uuid: ''
|
||||
})
|
||||
const rules = {
|
||||
account: [
|
||||
@@ -84,8 +107,21 @@ const rules = {
|
||||
message: '请输入密码',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getLoginCaptcha = async () => {
|
||||
const data = await loginCaptcha()
|
||||
formData.uuid = data.uuid
|
||||
codeImg.value = data.img
|
||||
}
|
||||
// 回车按键监听
|
||||
const handleEnter = () => {
|
||||
if (!formData.password) {
|
||||
@@ -101,7 +137,11 @@ const handleLogin = async () => {
|
||||
remember: remAccount.value,
|
||||
account: remAccount.value ? formData.account : ''
|
||||
})
|
||||
await userStore.login(formData)
|
||||
try {
|
||||
await userStore.login(formData)
|
||||
} catch (error) {
|
||||
getLoginCaptcha()
|
||||
}
|
||||
const {
|
||||
query: { redirect }
|
||||
} = route
|
||||
@@ -112,6 +152,7 @@ const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
|
||||
|
||||
onMounted(() => {
|
||||
const value = cache.get(ACCOUNT_KEY)
|
||||
getLoginCaptcha()
|
||||
if (value?.remember) {
|
||||
remAccount.value = value.remember
|
||||
formData.account = value.account
|
||||
@@ -125,6 +166,9 @@ onMounted(() => {
|
||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
||||
.login-card {
|
||||
height: 400px;
|
||||
:deep(.el-input-group__prepend) {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
54
admin/src/views/app/recharge/index.vue
Normal file
54
admin/src/views/app/recharge/index.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never" class="!border-none">
|
||||
<template #header>
|
||||
<span class="font-extrabold text-lg">充值设置</span>
|
||||
</template>
|
||||
<el-form :model="formData" label-width="120px">
|
||||
<el-form-item label="状态">
|
||||
<div>
|
||||
<el-radio-group v-model="formData.openRecharge" class="ml-4">
|
||||
<el-radio :label="1">开启</el-radio>
|
||||
<el-radio :label="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">关闭或开启充值功能,关闭后将不显示充值入口</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低充值金额">
|
||||
<div>
|
||||
<el-input
|
||||
v-model="formData.minRechargeMoney"
|
||||
placeholder="请输入最低充值金额"
|
||||
clearable
|
||||
/>
|
||||
<div class="form-tips">
|
||||
最低充值金额要求,不填或填0表示不限制最低充值金额
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<footer-btns v-perms="['marketing:recharge:save']">
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { getRechargeConfig, setRechargeConfig } from '@/api/app/recharge'
|
||||
import feedback from '@/utils/feedback'
|
||||
const formData = reactive({
|
||||
openRecharge: 1, //功能状态 1-开启 0-关闭
|
||||
minRechargeMoney: '' //最低充值金额
|
||||
})
|
||||
|
||||
const getConfig = async () => {
|
||||
const data = await getRechargeConfig()
|
||||
Object.assign(formData, data)
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
await setRechargeConfig(formData)
|
||||
feedback.msgSuccess('操作成功')
|
||||
getConfig()
|
||||
}
|
||||
getConfig()
|
||||
</script>
|
||||
@@ -87,7 +87,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="初始浏览量" prop="visit">
|
||||
<div>
|
||||
<el-input-number v-model="formData.visit" />
|
||||
<el-input-number v-model="formData.visit" :min="0" :max="9999" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章状态" required prop="isShow">
|
||||
|
||||
@@ -47,7 +47,7 @@ export const rules = reactive<FormRules>({
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/,
|
||||
/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/,
|
||||
message: '请输入合法的网址链接',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:填写微信开放平台开发配置,请前往微信开放平台创建应用并完成认证;APP应用配置主要用于APP微信登录和微信支付"
|
||||
title="温馨提示:填写微信开放平台开发配置,请前往微信开放平台创建应用并完成认证;网站应用配置主要用于网站微信登录和微信支付"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-form ref="formRef" :model="formData" label-width="160px">
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="font-medium mb-7">APP应用</div>
|
||||
<div class="font-medium mb-7">网站应用</div>
|
||||
<el-form-item label="AppID" prop="appId">
|
||||
<div class="w-80">
|
||||
<el-input v-model="formData.appId" placeholder="请输入AppID" />
|
||||
@@ -23,20 +23,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-tips">
|
||||
小程序账号登录微信公众平台,点击开发>开发设置->开发者ID,设置AppID和AppSecret
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<footer-btns v-perms="['channel:wx:save']">
|
||||
<footer-btns v-perms="['channel:op:save']">
|
||||
<el-button type="primary" @click="handelSave">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="wxDevConfig">
|
||||
import { getWxDevConfig, setWxDevConfig } from '@/api/channel/wx_dev'
|
||||
import { getWxDevConfig, setWxDevConfig } from '@/api/channel/wx_op'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const formData = reactive({
|
||||
104
admin/src/views/consumer/components/account-adjust.vue
Normal file
104
admin/src/views/consumer/components/account-adjust.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<popup
|
||||
ref="popupRef"
|
||||
title="余额调整"
|
||||
width="500px"
|
||||
@confirm="handleConfirm"
|
||||
:async="true"
|
||||
@close="popupClose"
|
||||
>
|
||||
<div class="pr-8">
|
||||
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
|
||||
<el-form-item label="当前余额">¥ {{ value }} </el-form-item>
|
||||
<el-form-item label="余额增减" required prop="action">
|
||||
<el-radio-group v-model="formData.action">
|
||||
<el-radio :label="1">增加余额</el-radio>
|
||||
<el-radio :label="2">扣减余额</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整余额" prop="num">
|
||||
<el-input
|
||||
:model-value="formData.num"
|
||||
placeholder="请输入调整的金额"
|
||||
type="number"
|
||||
@input="numberValidate"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整后余额"> ¥ {{ adjustmentMoney }} </el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import feedback from '@/utils/feedback'
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:show', value: boolean): void
|
||||
(event: 'confirm', value: any): void
|
||||
}>()
|
||||
const formData = reactive({
|
||||
action: 1, //变动类型 1-增加 2-减少
|
||||
num: '',
|
||||
remark: ''
|
||||
})
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
|
||||
const adjustmentMoney = computed(() => {
|
||||
return Number(props.value) + Number(formData.num) * (formData.action == 1 ? 1 : -1)
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
num: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入调整的金额'
|
||||
}
|
||||
]
|
||||
}
|
||||
const numberValidate = (value: string) => {
|
||||
if (value.includes('-')) {
|
||||
return feedback.msgError('请输入正整数')
|
||||
}
|
||||
formData.num = value
|
||||
}
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
emit('confirm', formData)
|
||||
}
|
||||
|
||||
const popupClose = () => {
|
||||
emit('update:show', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val) {
|
||||
popupRef.value?.open()
|
||||
} else {
|
||||
popupRef.value?.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(adjustmentMoney, (val) => {
|
||||
if (val < 0) {
|
||||
feedback.msgError('调整后余额需大于0')
|
||||
formData.num = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -5,9 +5,25 @@
|
||||
</el-card>
|
||||
<el-card class="mt-4 !border-none" header="基本资料" shadow="never">
|
||||
<el-form ref="formRef" class="ls-form" :model="formData" label-width="120px">
|
||||
<div class="bg-page py-5 pl-20 mb-10">
|
||||
<div class="mb-3 text-tx-regular">用户头像</div>
|
||||
<el-avatar :src="formData.avatar" :size="58" />
|
||||
<div class="bg-page flex py-5 mb-10 items-center">
|
||||
<div class="basis-40 flex flex-col justify-center items-center">
|
||||
<div class="mb-2 text-tx-regular">用户头像</div>
|
||||
<el-avatar :src="formData.avatar" :size="58" />
|
||||
</div>
|
||||
<div class="basis-40 flex flex-col justify-center items-center">
|
||||
<div class="text-tx-regular">账户余额</div>
|
||||
<div class="mt-2 flex items-center">
|
||||
¥{{ formData.user_money }}
|
||||
<el-button
|
||||
v-perms="['user.user/adjustMoney']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleAdjust(formData.user_money)"
|
||||
>
|
||||
调整
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item label="用户编号:"> {{ formData.sn }} </el-form-item>
|
||||
<el-form-item label="用户昵称:">
|
||||
@@ -80,6 +96,11 @@
|
||||
<el-form-item label="最近登录时间:"> {{ formData.lastLoginTime }} </el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<account-adjust
|
||||
v-model:show="adjustState.show"
|
||||
:value="adjustState.value"
|
||||
@confirm="handleConfirmAdjust"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,7 +109,7 @@ import type { FormInstance } from 'element-plus'
|
||||
import { getUserDetail, userEdit } from '@/api/consumer'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { isEmpty } from '@/utils/util'
|
||||
|
||||
import AccountAdjust from '../components/account-adjust.vue'
|
||||
const route = useRoute()
|
||||
const formData = reactive({
|
||||
avatar: '',
|
||||
@@ -105,7 +126,10 @@ const formData = reactive({
|
||||
})
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
|
||||
const adjustState = reactive({
|
||||
show: false,
|
||||
value: ''
|
||||
})
|
||||
const getDetails = async () => {
|
||||
const data = await getUserDetail({
|
||||
id: route.query.id
|
||||
@@ -126,6 +150,15 @@ const handleEdit = async (value: string, field: string) => {
|
||||
feedback.msgSuccess('编辑成功')
|
||||
getDetails()
|
||||
}
|
||||
const handleAdjust = (value: string) => {
|
||||
adjustState.show = true
|
||||
adjustState.value = value
|
||||
}
|
||||
const handleConfirmAdjust = async (value: any) => {
|
||||
await adjustMoney({ user_id: route.query.id, ...value })
|
||||
adjustState.show = false
|
||||
getDetails()
|
||||
}
|
||||
|
||||
getDetails()
|
||||
</script>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<el-table-column label="昵称" prop="nickname" min-width="100" />
|
||||
<el-table-column label="账号" prop="username" min-width="120" />
|
||||
<el-table-column label="手机号码" prop="mobile" min-width="100" />
|
||||
<el-table-column label="性别" prop="sex" min-width="100" />
|
||||
<!-- <el-table-column label="性别" prop="sex" min-width="100" /> -->
|
||||
<el-table-column label="注册来源" prop="channel" min-width="100" />
|
||||
<el-table-column label="注册时间" prop="createTime" min-width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
:is="widgets[widget?.name]?.attr"
|
||||
:content="widget?.content"
|
||||
:styles="widget?.styles"
|
||||
:type="type"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
@@ -23,6 +24,10 @@ const props = defineProps({
|
||||
widget: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'mobile' | 'pc'>,
|
||||
default: 'mobile'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
67
admin/src/views/decoration/component/pages/preview-pc.vue
Normal file
67
admin/src/views/decoration/component/pages/preview-pc.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="pages-preview">
|
||||
<div
|
||||
v-for="(widget, index) in pageData"
|
||||
:key="widget.id"
|
||||
class="relative"
|
||||
:class="{
|
||||
'cursor-pointer': !widget?.disabled
|
||||
}"
|
||||
@click="handleClick(widget, index)"
|
||||
>
|
||||
<div
|
||||
class="absolute w-full h-full z-[100] border-dashed"
|
||||
:class="{
|
||||
select: index == modelValue,
|
||||
'border-[#dcdfe6] border-2': !widget?.disabled
|
||||
}"
|
||||
:style="widget.styles"
|
||||
></div>
|
||||
<slot>
|
||||
<component
|
||||
:is="widgets[widget?.name]?.content"
|
||||
:content="widget.content"
|
||||
:styles="widget.styles"
|
||||
:key="widget.id"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import widgets from '../widgets'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
defineProps({
|
||||
pageData: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
|
||||
const handleClick = (widget: any, index: number) => {
|
||||
if (widget.disabled) return
|
||||
emit('update:modelValue', index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pages-preview {
|
||||
width: 460px;
|
||||
height: 360px;
|
||||
background: url(../../image/pc_index.png);
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
.select {
|
||||
@apply border-primary border-solid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,7 +33,15 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="mt-[18px]" label="图片链接">
|
||||
<link-picker v-model="item.link" />
|
||||
<link-picker
|
||||
v-if="type == 'mobile'"
|
||||
v-model="item.link"
|
||||
/>
|
||||
<el-input
|
||||
v-if="type == 'pc'"
|
||||
placeholder="请输入链接"
|
||||
v-model="item.link.path"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,6 +71,10 @@ const props = defineProps({
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'mobile' | 'pc'>,
|
||||
default: 'mobile'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="banner">
|
||||
<div class="banner-image">
|
||||
<decoration-img width="100%" height="170px" :src="getImage" fit="contain" />
|
||||
<div class="banner" :style="styles">
|
||||
<div class="banner-image w-full h-full">
|
||||
<decoration-img
|
||||
width="100%"
|
||||
:height="styles.height || height"
|
||||
:src="getImage"
|
||||
fit="contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,6 +23,10 @@ const props = defineProps({
|
||||
styles: {
|
||||
type: Object as PropType<OptionsType['styles']>,
|
||||
default: () => ({})
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '170px'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
BIN
admin/src/views/decoration/image/pc_index.png
Normal file
BIN
admin/src/views/decoration/image/pc_index.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 516 KiB |
90
admin/src/views/decoration/pc.vue
Normal file
90
admin/src/views/decoration/pc.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="decoration-pages min-w-[1100px]">
|
||||
<el-card shadow="never" class="!border-none flex-1 flex" :body-style="{ flex: 1 }">
|
||||
<div class="flex h-full items-start">
|
||||
<Menu v-model="activeMenu" :menus="menus" />
|
||||
<preview-pc class="mx-4" v-model="selectWidgetIndex" :pageData="getPageData" />
|
||||
<attr-setting class="flex-1" :widget="getSelectWidget" type="pc" />
|
||||
</div>
|
||||
</el-card>
|
||||
<footer-btns class="mt-4" :fixed="false" v-perms="['decorate:pages:save']">
|
||||
<el-button type="primary" @click="setData">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="decorationPc">
|
||||
import Menu from './component/pages/menu.vue'
|
||||
import PreviewPc from './component/pages/preview-pc.vue'
|
||||
import AttrSetting from './component/pages/attr-setting.vue'
|
||||
import widgets from './component/widgets'
|
||||
import { getDecoratePages, setDecoratePages } from '@/api/decoration'
|
||||
import { getNonDuplicateID } from '@/utils/util'
|
||||
enum pagesTypeEnum {
|
||||
HOME = '4'
|
||||
}
|
||||
|
||||
const generatePageData = (widgetNames: string[]) => {
|
||||
return widgetNames.map((widgetName) => {
|
||||
const options = {
|
||||
id: getNonDuplicateID(),
|
||||
...(widgets[widgetName]?.options() || {})
|
||||
}
|
||||
return options
|
||||
})
|
||||
}
|
||||
|
||||
const menus: Record<
|
||||
string,
|
||||
{
|
||||
id: number
|
||||
name: string
|
||||
pageData: any[]
|
||||
}
|
||||
> = reactive({
|
||||
[pagesTypeEnum.HOME]: {
|
||||
id: 4,
|
||||
pageType: 4,
|
||||
name: 'pc首页装修',
|
||||
pageData: []
|
||||
}
|
||||
})
|
||||
|
||||
const activeMenu = ref('4')
|
||||
const selectWidgetIndex = ref(0)
|
||||
const getPageData = computed(() => {
|
||||
return menus[activeMenu.value]?.pageData ?? []
|
||||
})
|
||||
const getSelectWidget = computed(() => {
|
||||
return menus[activeMenu.value]?.pageData[selectWidgetIndex.value] ?? ''
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const data = await getDecoratePages({ id: activeMenu.value })
|
||||
menus[String(data.id)].pageData = JSON.parse(data.pageData)
|
||||
selectWidgetIndex.value = getPageData.value.findIndex((item) => !item.disabled)
|
||||
}
|
||||
|
||||
const setData = async () => {
|
||||
await setDecoratePages({
|
||||
...menus[activeMenu.value],
|
||||
pageData: JSON.stringify(menus[activeMenu.value].pageData)
|
||||
})
|
||||
getData()
|
||||
}
|
||||
watch(
|
||||
activeMenu,
|
||||
() => {
|
||||
selectWidgetIndex.value = getPageData.value.findIndex((item) => !item.disabled)
|
||||
getData()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.decoration-pages {
|
||||
min-height: calc(100vh - var(--navbar-height) - 80px);
|
||||
@apply flex flex-col;
|
||||
}
|
||||
</style>
|
||||
@@ -218,11 +218,50 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="生成方式" prop="gen.genType">
|
||||
<el-radio-group v-model="formData.gen.genType">
|
||||
<el-radio :label="GenType.ZIP">压缩包下载</el-radio>
|
||||
<el-radio :label="GenType.CUSTOM_PATH">自定义路径</el-radio>
|
||||
</el-radio-group>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.gen.genType">
|
||||
<el-radio :label="GenType.ZIP">压缩包下载</el-radio>
|
||||
<el-radio :label="GenType.CUSTOM_PATH">自定义路径</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">压縮包下载方式暂不支持自动构建菜单权限</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单构建" prop="gen.menuStatus" required>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.gen.menuStatus">
|
||||
<el-radio :label="1">自动构建</el-radio>
|
||||
<el-radio :label="0">手动添加</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">
|
||||
自动构建:自动执行生成菜单sql。 手动添加:自行添加菜单
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="父级菜单" prop="gen.menuPid">
|
||||
<el-tree-select
|
||||
class="w-80"
|
||||
v-model="formData.gen.menuPid"
|
||||
:data="optionsData.menu"
|
||||
clearable
|
||||
node-key="id"
|
||||
:props="{
|
||||
label: 'menuName'
|
||||
}"
|
||||
default-expand-all
|
||||
placeholder="请选择父级菜单"
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="gen.menuName">
|
||||
<div class="w-80">
|
||||
<el-input
|
||||
v-model="formData.gen.menuName"
|
||||
placeholder="请输入菜单名称"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="formData.gen.genType == GenType.CUSTOM_PATH"
|
||||
label="自定义路径"
|
||||
@@ -275,7 +314,12 @@
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="关联配置" name="relation">
|
||||
<el-form-item label="关联子表的表名" prop="gen.subTableName">
|
||||
<el-select class="w-80" v-model="formData.gen.subTableName" clearable>
|
||||
<el-select
|
||||
class="w-80"
|
||||
v-model="formData.gen.subTableName"
|
||||
clearable
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in optionsData.dataTable"
|
||||
:key="item.tableName"
|
||||
@@ -285,9 +329,14 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="子表关联的外键名 " prop="gen.subTableFk">
|
||||
<el-select class="w-80" v-model="formData.gen.subTableFk" clearable>
|
||||
<el-select
|
||||
class="w-80"
|
||||
v-model="formData.gen.subTableFk"
|
||||
clearable
|
||||
:loading="columnLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in formData.column"
|
||||
v-for="item in tableColumn"
|
||||
:key="item.id"
|
||||
:value="item.columnName"
|
||||
:label="`${item.columnName}:${item.columnComment}`"
|
||||
@@ -295,13 +344,14 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联表主键 " prop="gen.subTableFr">
|
||||
<div class="w-80">
|
||||
<el-input
|
||||
v-model="formData.gen.subTableFr"
|
||||
placeholder="请输入关联表主键"
|
||||
clearable
|
||||
<el-select class="w-80" v-model="formData.gen.subTableFr" clearable>
|
||||
<el-option
|
||||
v-for="item in formData.column"
|
||||
:key="item.id"
|
||||
:value="item.columnName"
|
||||
:label="`${item.columnName}:${item.columnComment}`"
|
||||
/>
|
||||
</div>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -314,7 +364,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="tableEdit">
|
||||
import { dataTableAll, generateEdit, tableDetail } from '@/api/tools/code'
|
||||
import { dataTableAll, generateEdit, tableDetail, dataTableToColumn } from '@/api/tools/code'
|
||||
import { dictTypeAll } from '@/api/setting/dict'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import feedback from '@/utils/feedback'
|
||||
@@ -356,7 +406,10 @@ const formData = reactive({
|
||||
subTableFr: '',
|
||||
treeParent: '',
|
||||
treePrimary: '',
|
||||
treeName: ''
|
||||
treeName: '',
|
||||
menuName: '',
|
||||
menuStatus: 0,
|
||||
menuPid: 0
|
||||
}
|
||||
})
|
||||
|
||||
@@ -377,10 +430,12 @@ const getDetails = async () => {
|
||||
const data = await tableDetail({
|
||||
id: route.query.id
|
||||
})
|
||||
|
||||
Object.keys(formData).forEach((key) => {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
})
|
||||
getTableColumn()
|
||||
}
|
||||
|
||||
const { optionsData } = useDictOptions<{
|
||||
@@ -394,9 +449,9 @@ const { optionsData } = useDictOptions<{
|
||||
menu: {
|
||||
api: menuLists,
|
||||
transformData(data: any) {
|
||||
const menu = { id: 0, name: '顶级', children: [] }
|
||||
const menu = { id: 0, menuName: '顶级', children: [] }
|
||||
menu.children = data
|
||||
return menu
|
||||
return [menu]
|
||||
}
|
||||
},
|
||||
dataTable: {
|
||||
@@ -404,6 +459,19 @@ const { optionsData } = useDictOptions<{
|
||||
}
|
||||
})
|
||||
|
||||
const columnLoading = ref(false)
|
||||
const tableColumn = ref<any[]>([])
|
||||
const getTableColumn = async () => {
|
||||
columnLoading.value = true
|
||||
const res = await dataTableToColumn({ tableName: formData.gen.subTableName })
|
||||
columnLoading.value = false
|
||||
tableColumn.value = res
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
formData.gen.subTableFk = ''
|
||||
getTableColumn()
|
||||
}
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
|
||||
101
admin/src/views/finance/balance_details.vue
Normal file
101
admin/src/views/finance/balance_details.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:用户账户变动记录"
|
||||
:closable="false"
|
||||
show-icon
|
||||
></el-alert>
|
||||
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item label="用户信息">
|
||||
<el-input
|
||||
class="w-[280px]"
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="请输入用户编号/昵称/手机号"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="变动类型">
|
||||
<el-select class="w-[280px]" v-model="queryParams.type">
|
||||
<el-option label="全部" value />
|
||||
<el-option
|
||||
v-for="(value, key) in changeType"
|
||||
:key="key"
|
||||
:label="value"
|
||||
:value="key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="记录时间">
|
||||
<daterange-picker
|
||||
v-model:startTime="queryParams.startTime"
|
||||
v-model:endTime="queryParams.endTime"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="用户编号" prop="userSn" min-width="100" />
|
||||
<el-table-column label="用户昵称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<image-contain
|
||||
class="flex-none mr-2"
|
||||
:src="row.avatar"
|
||||
:width="40"
|
||||
:height="40"
|
||||
preview-teleported
|
||||
fit="contain"
|
||||
/>
|
||||
{{ row.nickname }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变动金额" prop="changeAmount" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-error': row.action == 2 }">
|
||||
{{ row.changeAmount }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余金额" prop="leftAmount" min-width="100" />
|
||||
<el-table-column label="变动类型" prop="changeType" min-width="120" />
|
||||
|
||||
<el-table-column label="来源单号" prop="sourceSn" min-width="100" />
|
||||
<el-table-column label="记录时间" prop="createTime" min-width="120" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="articleLists">
|
||||
import { accountLog } from '@/api/finance'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
const queryParams = reactive({
|
||||
keyword: '',
|
||||
type: '',
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
})
|
||||
|
||||
const changeType = ref<any[]>([])
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: accountLog,
|
||||
params: queryParams,
|
||||
afterRequest(res) {
|
||||
changeType.value = res.extend?.changeType
|
||||
}
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
68
admin/src/views/finance/component/refund-log.vue
Normal file
68
admin/src/views/finance/component/refund-log.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="code-preview">
|
||||
<el-dialog v-model="show" width="760px" title="退款日志">
|
||||
<el-table size="large" v-loading="loading" :data="logLists" height="500">
|
||||
<el-table-column label="流水单号" prop="sn" min-width="190" />
|
||||
<el-table-column label="退款金额" min-width="110">
|
||||
<template #default="{ row }"> ¥{{ row.refundAmount }} </template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款状态" prop="" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning" v-if="row.refundStatus == 0">
|
||||
{{ row.refundStatusMsg }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.refundStatus == 1">
|
||||
{{ row.refundStatusMsg }}
|
||||
</el-tag>
|
||||
<el-tag type="danger" v-if="row.refundStatus == 2">
|
||||
{{ row.refundStatusMsg }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="记录时间" prop="createTime" min-width="180" />
|
||||
<el-table-column label="操作人" prop="handler" min-width="120" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { refundLog } from '@/api/finance'
|
||||
const loading = ref(false)
|
||||
const logLists = ref([])
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
refundId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const show = computed<boolean>({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const getRefundLog = async () => {
|
||||
loading.value = true
|
||||
logLists.value = []
|
||||
try {
|
||||
const res = await refundLog({
|
||||
id: props.refundId
|
||||
})
|
||||
logLists.value = res
|
||||
} catch (error) {}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
getRefundLog()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
144
admin/src/views/finance/recharge_record.vue
Normal file
144
admin/src/views/finance/recharge_record.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:用户充值记录"
|
||||
:closable="false"
|
||||
show-icon
|
||||
></el-alert>
|
||||
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item label="充值单号">
|
||||
<el-input
|
||||
class="w-[280px]"
|
||||
v-model="queryParams.sn"
|
||||
placeholder="请输入充值单号"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户信息">
|
||||
<el-input
|
||||
class="w-[280px]"
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="请输入用户编号/昵称/手机号"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付方式">
|
||||
<el-select class="w-[280px]" v-model="queryParams.payWay">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="微信支付" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付状态">
|
||||
<el-select class="w-[280px]" v-model="queryParams.payStatus">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="未支付" :value="0" />
|
||||
<el-option label="已支付" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="下单时间">
|
||||
<daterange-picker
|
||||
v-model:startTime="queryParams.startTime"
|
||||
v-model:endTime="queryParams.endTime"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
<export-data
|
||||
class="ml-2.5"
|
||||
:fetch-fun="rechargeLists"
|
||||
:params="queryParams"
|
||||
:page-size="pager.size"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="用户信息" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<image-contain
|
||||
class="flex-none mr-2"
|
||||
:src="row.avatar"
|
||||
:width="40"
|
||||
:height="40"
|
||||
preview-teleported
|
||||
fit="contain"
|
||||
/>
|
||||
{{ row.nickname }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值单号" prop="orderSn" min-width="190" />
|
||||
<el-table-column label="充值金额" prop="orderAmount" min-width="100">
|
||||
</el-table-column>
|
||||
<el-table-column label="支付方式" prop="payWay" min-width="100" />
|
||||
<el-table-column label="支付状态" prop="" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
:class="{
|
||||
'text-error': row.payStatus == 0
|
||||
}"
|
||||
>
|
||||
{{ row.payStatus == 0 ? '未支付' : '已支付' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提交时间" prop="createTime" min-width="180" />
|
||||
<el-table-column label="支付时间" prop="payTime" min-width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.payStatus == 1 && row.isRefund == 0"
|
||||
v-perms="['finance:recharger:refund']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleRefund(row.id)"
|
||||
>
|
||||
退款
|
||||
</el-button>
|
||||
<el-button v-if="row.payStatus == 1 && row.isRefund == 1" link>
|
||||
{{ row.refundStatusMsg }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="articleLists">
|
||||
import { rechargeLists, refund } from '@/api/finance'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
const queryParams = reactive({
|
||||
sn: '',
|
||||
keyword: '',
|
||||
payStatus: '',
|
||||
payWay: '',
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: rechargeLists,
|
||||
params: queryParams
|
||||
})
|
||||
const handleRefund = async (id: number) => {
|
||||
await feedback.confirm('确认退款?')
|
||||
await refund({
|
||||
id
|
||||
})
|
||||
feedback.msgSuccess('操作成功')
|
||||
getLists()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
226
admin/src/views/finance/refund_record.vue
Normal file
226
admin/src/views/finance/refund_record.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none mb-4" shadow="never">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-1/2 md:w-1/4">
|
||||
<div class="leading-10">累计退款金额 (元)</div>
|
||||
<div class="text-6xl">{{ refundData.totalRefundAmount }}</div>
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4">
|
||||
<div class="leading-10">退款中金额 (元)</div>
|
||||
<div class="text-6xl">{{ refundData.ingRefundAmount }}</div>
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4">
|
||||
<div class="leading-10">退款成功金额 (元)</div>
|
||||
<div class="text-6xl">{{ refundData.successRefundAmount }}</div>
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4">
|
||||
<div class="leading-10">退款失败金额 (元)</div>
|
||||
<div class="text-6xl">{{ refundData.errorRefundAmount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item label="退款单号">
|
||||
<el-input
|
||||
class="w-[280px]"
|
||||
v-model="queryParams.sn"
|
||||
placeholder="请输入退款单号"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="来源单号">
|
||||
<el-input
|
||||
class="w-[280px]"
|
||||
v-model="queryParams.orderSn"
|
||||
placeholder="请输入来源单号"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户信息">
|
||||
<el-input
|
||||
class="w-[280px]"
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="请输入用户信息"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退款类型">
|
||||
<el-select class="w-[280px]" v-model="queryParams.refundType">
|
||||
<el-option label="全部" value />
|
||||
<el-option label="后台退款" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="记录时间">
|
||||
<daterange-picker
|
||||
v-model:startTime="queryParams.startTime"
|
||||
v-model:endTime="queryParams.endTime"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
<!-- <export-data
|
||||
class="ml-2.5"
|
||||
:fetch-fun="refundRecord"
|
||||
:params="queryParams"
|
||||
:page-size="pager.size"
|
||||
/> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in tabLists"
|
||||
:label="`${item.name}(${pager.extend[item.numKey] ?? 0})`"
|
||||
:name="index"
|
||||
:key="index"
|
||||
>
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="退款单号" prop="sn" min-width="190" />
|
||||
<el-table-column label="用户信息" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<image-contain
|
||||
class="flex-none mr-2"
|
||||
:src="row.avatar"
|
||||
:width="40"
|
||||
:height="40"
|
||||
preview-teleported
|
||||
fit="contain"
|
||||
/>
|
||||
{{ row.nickname }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源单号" prop="orderSn" min-width="190" />
|
||||
<el-table-column label="退款金额" min-width="100">
|
||||
<template #default="{ row }"> ¥ {{ row.refundAmount }} </template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="退款类型" prop="refundTypeMsg" min-width="100" />
|
||||
<el-table-column label="退款状态" prop="" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning" v-if="row.refundStatus == 0">
|
||||
{{ row.refundStatusMsg }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.refundStatus == 1">
|
||||
{{ row.refundStatusMsg }}
|
||||
</el-tag>
|
||||
<el-tag type="danger" v-if="row.refundStatus == 2">
|
||||
{{ row.refundStatusMsg }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="记录时间" prop="createTime" min-width="180" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['finance:refund:log']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleShowRefundLog(row.id)"
|
||||
>
|
||||
退款日志
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.refundStatus == 2"
|
||||
v-perms="['finance:recharger:refundAgain']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleRefund(row.id)"
|
||||
>
|
||||
重新退款
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
<refund-log v-model="showRefundLog" :refund-id="selectRefundId" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="articleLists">
|
||||
import { refundRecord, refundAgain } from '@/api/finance'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
import RefundLog from './component/refund-log.vue'
|
||||
const queryParams = reactive({
|
||||
sn: '',
|
||||
orderSn: '',
|
||||
keyword: '',
|
||||
refundType: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
type: ''
|
||||
})
|
||||
const refundData = reactive({
|
||||
errorRefundAmount: 0,
|
||||
ingRefundAmount: 0,
|
||||
successRefundAmount: 0,
|
||||
totalRefundAmount: 0
|
||||
})
|
||||
const showRefundLog = ref(false)
|
||||
const selectRefundId = ref(0)
|
||||
const activeTab = ref(0)
|
||||
const tabLists = ref([
|
||||
{
|
||||
name: '全部',
|
||||
type: '',
|
||||
numKey: 'total'
|
||||
},
|
||||
{
|
||||
name: '退款中',
|
||||
type: 0,
|
||||
numKey: 'ing'
|
||||
},
|
||||
{
|
||||
name: '退款成功',
|
||||
type: 1,
|
||||
numKey: 'success'
|
||||
},
|
||||
{
|
||||
name: '退款失败',
|
||||
type: 2,
|
||||
numKey: 'error'
|
||||
}
|
||||
])
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: refundRecord,
|
||||
params: queryParams,
|
||||
afterRequest(res) {
|
||||
Object.assign(refundData, res.extend.stat)
|
||||
}
|
||||
})
|
||||
|
||||
const handleRefund = async (id: number) => {
|
||||
await feedback.confirm('确认重新退款?')
|
||||
await refundAgain({
|
||||
id
|
||||
})
|
||||
feedback.msgSuccess('操作成功')
|
||||
getLists()
|
||||
}
|
||||
|
||||
const handleShowRefundLog = async (id: number) => {
|
||||
showRefundLog.value = true
|
||||
selectRefundId.value = id
|
||||
}
|
||||
const handleTabChange = (index: any) => {
|
||||
queryParams.type = tabLists.value[index].type as string
|
||||
resetPage()
|
||||
}
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -60,6 +60,7 @@
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.menuType !== MenuEnum.BUTTON"
|
||||
v-perms="['system:menu:add']"
|
||||
type="primary"
|
||||
link
|
||||
|
||||
279
admin/src/views/setting/pay/config/edit.vue
Normal file
279
admin/src/views/setting/pay/config/edit.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="edit-popup">
|
||||
<popup
|
||||
ref="popupRef"
|
||||
:title="popupTitle"
|
||||
:async="true"
|
||||
width="550px"
|
||||
@confirm="handleSubmit"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" label-width="84px" :rules="formRules">
|
||||
<el-form-item label="支付方式">
|
||||
<el-radio :label="formData.name" :model-value="formData.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示名称" prop="showName">
|
||||
<el-input v-model="formData.showName" placeholder="请输入显示名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示图标" prop="icon">
|
||||
<div>
|
||||
<material-picker :limit="1" :disabled="false" v-model="formData.icon" />
|
||||
<span class="form-tips">建议尺寸:200*200px</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<template v-if="formData.way == PayWayEnum.WECHAT">
|
||||
<el-form-item prop="params.interface_version" label="微信支付接口版本">
|
||||
<div>
|
||||
<el-radio-group v-model="formData.params.interface_version">
|
||||
<el-radio label="v3"></el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">暂时只支持V3版本</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商户类型" prop="params.merchant_type">
|
||||
<div>
|
||||
<el-radio-group v-model="formData.params.merchant_type">
|
||||
<el-radio label="ordinary_merchant">普通商户</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">
|
||||
暂时只支持普通商户类型,服务商户类型模式暂不支持
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="微信支付商户号" prop="params.mch_id">
|
||||
<div class="flex-1">
|
||||
<el-input
|
||||
v-model="formData.params.mch_id"
|
||||
placeholder="请输入微信支付商户号"
|
||||
/>
|
||||
<div class="form-tips">微信支付商户号(MCHID)</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商户API密钥" prop="params.pay_sign_key">
|
||||
<el-input
|
||||
v-model="formData.params.pay_sign_key"
|
||||
placeholder="请输入微信支付商户API密钥"
|
||||
/>
|
||||
<span class="form-tips">微信支付商户API密钥(paySignKey)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="微信支付证书" prop="params.private_cert">
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="3"
|
||||
v-model="formData.params.private_cert"
|
||||
placeholder="请输入微信支付证书"
|
||||
/>
|
||||
|
||||
<span class="form-tips">
|
||||
微信支付证书(apiclient_cert.pem),前往微信商家平台生成并黏贴至此处
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="微信支付证书密钥" prop="params.private_key">
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="3"
|
||||
v-model="formData.params.private_key"
|
||||
placeholder="请输入微信支付证书密钥"
|
||||
/>
|
||||
<span class="form-tips">
|
||||
微信支付证书密钥(apiclient_key.pem),前往微信商家平台生成并黏贴至此处
|
||||
</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<!-- <template v-if="formData.way == PayWayEnum.ALIPAY">
|
||||
<el-form-item label="模式" prop="params.mode">
|
||||
<div>
|
||||
<el-radio-group v-model="formData.params.mode">
|
||||
<el-radio label="normal_mode">普通模式</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">暂时仅支持支付宝普通模式</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商户类型" prop="params.merchant_type">
|
||||
<div>
|
||||
<el-radio-group v-model="formData.params.merchant_type">
|
||||
<el-radio label="ordinary_merchant">普通商户</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tips">
|
||||
暂时只支持普通商户类型,服务商户类型模式暂不支持
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="应用ID" prop="params.app_id">
|
||||
<div class="flex-1">
|
||||
<el-input
|
||||
v-model="formData.params.app_id"
|
||||
placeholder="请输入支付宝应用ID"
|
||||
/>
|
||||
<span class="form-tips"> 支付宝应用APP_ID </span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="应用私钥" prop="params.private_key">
|
||||
<div class="flex-1">
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="3"
|
||||
v-model="formData.params.private_key"
|
||||
placeholder="请输入支付宝应用私钥"
|
||||
/>
|
||||
<span class="form-tips">支付宝应用私钥(private_key) </span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付宝公钥" prop="params.ali_public_key">
|
||||
<div class="flex-1">
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="3"
|
||||
v-model="formData.params.ali_public_key"
|
||||
placeholder="请输入支付宝公钥"
|
||||
/>
|
||||
<span class="form-tips">支付宝公钥(ali_public_key) </span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template> -->
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<div>
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
|
||||
<div class="form-tips">默认为0, 数值越大越排前</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</popup>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { getPayConfig, setPayConfig } from '@/api/setting/pay'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
const emit = defineEmits(['success', 'close'])
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
enum PayWayEnum {
|
||||
BALANCE = 1,
|
||||
WECHAT = 2,
|
||||
ALIPAY = 3
|
||||
}
|
||||
const popupTitle = computed(() => {
|
||||
switch (formData.way) {
|
||||
case PayWayEnum.BALANCE:
|
||||
return '余额支付'
|
||||
case PayWayEnum.WECHAT:
|
||||
return '微信支付'
|
||||
case PayWayEnum.ALIPAY:
|
||||
return '支付宝支付'
|
||||
}
|
||||
})
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
way: 0,
|
||||
name: '',
|
||||
showName: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
remark: '',
|
||||
params: {
|
||||
interface_version: '',
|
||||
merchant_type: '',
|
||||
mch_id: '',
|
||||
pay_sign_key: '',
|
||||
private_cert: '',
|
||||
private_key: '',
|
||||
mode: '',
|
||||
app_id: '',
|
||||
ali_public_key: ''
|
||||
}
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
showName: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入显示名称'
|
||||
}
|
||||
],
|
||||
'params.mch_id': [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入微信支付商户号'
|
||||
}
|
||||
],
|
||||
'params.pay_sign_key': [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入微信支付商户API密钥'
|
||||
}
|
||||
],
|
||||
'params.private_cert': [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入微信支付证书'
|
||||
}
|
||||
],
|
||||
'params.private_key': [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入微信支付证书密钥'
|
||||
}
|
||||
],
|
||||
'params.app_id': [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入支付宝应用ID'
|
||||
}
|
||||
],
|
||||
'params.ali_public_key': [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入支付宝公钥'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
await setPayConfig(formData)
|
||||
feedback.msgSuccess('操作成功')
|
||||
popupRef.value?.close()
|
||||
emit('success')
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
popupRef.value?.open()
|
||||
}
|
||||
|
||||
const setFormData = (data: Record<any, any>) => {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
//@ts-ignore
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getDetail = async (row: Record<string, any>) => {
|
||||
const data = await getPayConfig({
|
||||
id: row.id
|
||||
})
|
||||
setFormData(data)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
setFormData,
|
||||
getDetail
|
||||
})
|
||||
</script>
|
||||
63
admin/src/views/setting/pay/config/index.vue
Normal file
63
admin/src/views/setting/pay/config/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:设置系统支持的支付方式"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="mt-4 !border-none">
|
||||
<div>
|
||||
<el-table :data="payConfigList">
|
||||
<el-table-column prop="name" label="支付方式" min-width="150" />
|
||||
<el-table-column prop="showName" label="显示名称" min-width="150" />
|
||||
<el-table-column label="图标" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.icon"
|
||||
alt="图标"
|
||||
style="width: 34px; height: 34px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" min-width="150" />
|
||||
<el-table-column label="操作" min-width="80" fixed="right">
|
||||
<!-- 操作 -->
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['setting:payment:editConfig']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
配置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getConfig" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getPayConfigLists } from '@/api/setting/pay'
|
||||
import EditPopup from './edit.vue'
|
||||
const payConfigList = ref<any[]>([])
|
||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||
const showEdit = ref(false)
|
||||
const getConfig = async () => {
|
||||
const data = await getPayConfigLists()
|
||||
payConfigList.value = data
|
||||
}
|
||||
const handleEdit = async (data: any) => {
|
||||
showEdit.value = true
|
||||
await nextTick()
|
||||
editRef.value?.open()
|
||||
editRef.value?.getDetail(data)
|
||||
}
|
||||
getConfig()
|
||||
</script>
|
||||
138
admin/src/views/setting/pay/method/index.vue
Normal file
138
admin/src/views/setting/pay/method/index.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
v-perms="['setting:payment:editMethod']"
|
||||
@click="handelSetupPayWay"
|
||||
>
|
||||
设置支付方式
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="mt-4 !border-none"
|
||||
v-for="(value, scene) in payWay"
|
||||
:key="scene"
|
||||
>
|
||||
<div>
|
||||
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.MP_WEIXIN">
|
||||
微信小程序
|
||||
<span class="form-tips ml-[10px]">在微信小程序中付款的场景</span>
|
||||
</div>
|
||||
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.OA">
|
||||
微信公众号
|
||||
<span class="form-tips ml-[10px]">
|
||||
在微信公众号H5页面中付款的场景,公众号类型一般为服务号
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.H5">
|
||||
H5支付
|
||||
<span class="form-tips ml-[10px]">在浏览器H5页面中付款的场景</span>
|
||||
</div>
|
||||
<!-- <div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.PC">
|
||||
PC支付
|
||||
<span class="form-tips ml-[10px]">在浏览器PC页面中付款的场景</span>
|
||||
</div>
|
||||
<div class="text-lg mb-[24px]" v-if="scene == PaySceneEnum.APP">
|
||||
APP支付
|
||||
<span class="form-tips ml-[10px]">在APP付款的场景</span>
|
||||
</div> -->
|
||||
<el-table v-if="value.length" :data="value" style="width: 100%">
|
||||
<el-table-column label="图标" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.icon"
|
||||
alt="图标"
|
||||
style="width: 34px; height: 34px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="支付方式" min-width="150" />
|
||||
<el-table-column label="默认支付" min-width="150">
|
||||
<template #default="{ row, $index }">
|
||||
<div>
|
||||
<template v-if="setupPayWay">
|
||||
<el-radio
|
||||
v-model="row.isDefault"
|
||||
:label="1"
|
||||
@change="changePayDefault($index, scene)"
|
||||
>
|
||||
设为默认
|
||||
</el-radio>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag v-if="row.isDefault == 1">默认</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开启状态" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="setupPayWay"
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ row.status == 1 ? '开启' : '关闭' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
<footer-btns v-if="setupPayWay">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</footer-btns>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getPayWay, setPayWay } from '@/api/setting/pay'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { cloneDeep } from 'lodash'
|
||||
enum PaySceneEnum {
|
||||
MP_WEIXIN,
|
||||
OA,
|
||||
H5
|
||||
}
|
||||
const payWay = ref<Record<number, any[]>>({})
|
||||
const setupPayWay = ref(false)
|
||||
let defaultPayWay = {}
|
||||
const getConfig = async () => {
|
||||
payWay.value = await getPayWay()
|
||||
defaultPayWay = cloneDeep(payWay.value)
|
||||
}
|
||||
|
||||
const handelSetupPayWay = () => {
|
||||
setupPayWay.value = true
|
||||
}
|
||||
|
||||
const changePayDefault = (index: number, scene: number) => {
|
||||
payWay.value[scene].forEach((item: any) => {
|
||||
item.isDefault = 0
|
||||
})
|
||||
payWay.value[scene][index].isDefault = 1
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
payWay.value = cloneDeep(defaultPayWay)
|
||||
setupPayWay.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await setPayWay({
|
||||
data: payWay.value
|
||||
})
|
||||
setupPayWay.value = false
|
||||
feedback.msgSuccess('操作成功')
|
||||
getConfig()
|
||||
}
|
||||
|
||||
getConfig()
|
||||
</script>
|
||||
81
admin/src/views/setting/system/login_log.vue
Normal file
81
admin/src/views/setting/system/login_log.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<!-- 系统日志 -->
|
||||
<template>
|
||||
<div class="login-log">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="ls-form" :model="formData" inline>
|
||||
<el-form-item label="用户名称">
|
||||
<el-input
|
||||
class="w-[280px]"
|
||||
placeholder="请输入"
|
||||
v-model="formData.username"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select class="w-[280px]" v-model="formData.status" placeholder="请选择">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="失败" :value="0" />
|
||||
<el-option label="成功" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录时间">
|
||||
<daterange-picker
|
||||
v-model:startTime="formData.startTime"
|
||||
v-model:endTime="formData.endTime"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
|
||||
<div>
|
||||
<el-table :data="pager.lists" size="large">
|
||||
<el-table-column label="用户名称" prop="username" min-width="120" />
|
||||
<el-table-column label="登录地址" prop="ip" min-width="120" />
|
||||
<el-table-column label="浏览器" prop="browser" min-width="120" />
|
||||
<el-table-column label="摱作系統" prop="os" min-width="120" />
|
||||
<el-table-column prop="status" label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status == 1" type="success">成功</el-tag>
|
||||
<el-tag v-if="row.status == 0" type="danger">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="登录时间" prop="createTime" min-width="180" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="flex mt-4 justify-end">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="journal">
|
||||
import { loginLogLists } from '@/api/setting/system'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
|
||||
// 查询表单
|
||||
const formData = ref({
|
||||
username: '',
|
||||
status: '',
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
})
|
||||
|
||||
const { pager, getLists, resetParams, resetPage } = usePaging({
|
||||
fetchFun: loginLogLists,
|
||||
params: formData.value
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -21,10 +21,10 @@
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务分组" prop="groups">
|
||||
<el-form-item label="任务分组" prop="types">
|
||||
<el-select
|
||||
class="w-80"
|
||||
v-model="formData.groups"
|
||||
v-model="formData.types"
|
||||
clearable
|
||||
placeholder="请选择任务分组"
|
||||
>
|
||||
@@ -60,7 +60,7 @@
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="执行策略" prop="groups">
|
||||
<el-form-item label="执行策略" prop="strategy">
|
||||
<el-radio-group v-model="formData.strategy">
|
||||
<el-radio :label="1"> 立即执行 </el-radio>
|
||||
<el-radio :label="2"> 执行一次 </el-radio>
|
||||
@@ -95,7 +95,7 @@ const router = useRouter()
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
groups: '',
|
||||
types: '',
|
||||
command: '',
|
||||
rules: '',
|
||||
status: 1,
|
||||
|
||||
@@ -52,6 +52,44 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="!border-none mt-4">
|
||||
<div class="text-xl font-medium mb-[20px]">PC端设置</div>
|
||||
<el-form-item label="PC端LOGO" prop="pcLogo">
|
||||
<div>
|
||||
<material-picker v-model="formData.pcLogo" :limit="1" />
|
||||
<div class="form-tips">建议尺寸:120*28px,支持jpg,jpeg,png格式</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网站标题" prop="pcTitle">
|
||||
<div class="w-80">
|
||||
<el-input
|
||||
v-model.trim="formData.pcTitle"
|
||||
placeholder="请输入PC端网站标题"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网站图标" prop="pcIco">
|
||||
<div>
|
||||
<material-picker v-model="formData.pcIco" :limit="1" />
|
||||
<div class="form-tips">建议尺寸:100*100像素,支持jpg,jpeg,png格式</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网站描述" prop="pcDesc">
|
||||
<div class="w-80">
|
||||
<el-input v-model.trim="formData.pcDesc" placeholder="请输入PC端网站描述" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网站关键词" prop="pcKeywords">
|
||||
<div class="w-80">
|
||||
<el-input
|
||||
v-model.trim="formData.pcKeywords"
|
||||
placeholder="请输入PC端网站关键词"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<footer-btns v-perms="['setting:website:save']">
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
@@ -73,7 +111,12 @@ const formData = reactive({
|
||||
logo: '', // 网站logo
|
||||
backdrop: '', // 登录页广告图
|
||||
shopName: '',
|
||||
shopLogo: ''
|
||||
shopLogo: '',
|
||||
pcDesc: '',
|
||||
pcIco: '',
|
||||
pcKeywords: '',
|
||||
pcLogo: '',
|
||||
pcTitle: ''
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
@@ -119,6 +162,27 @@ const rules = {
|
||||
message: '请选择商城LOGO',
|
||||
trigger: ['change']
|
||||
}
|
||||
],
|
||||
pcLogo: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择PC端LOGO',
|
||||
trigger: ['change']
|
||||
}
|
||||
],
|
||||
pcTitle: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入PC端网站标题',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
pcIco: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择PC端网站图标',
|
||||
trigger: ['change']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
63
admin/src/views/template/component/file.vue
Normal file
63
admin/src/views/template/component/file.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card header="基础使用" shadow="none" class="!border-none">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="flex m-4">
|
||||
<div class="mr-4">选择图片:</div>
|
||||
<material-picker v-model="state.value1" />
|
||||
</div>
|
||||
<div class="flex m-4">
|
||||
<div class="mr-4">选择视频:</div>
|
||||
<material-picker type="video" v-model="state.value3" />
|
||||
</div>
|
||||
<div class="flex flex-1 m-4">
|
||||
<div class="mr-4">多张图片:</div>
|
||||
<div class="flex-1">
|
||||
<!-- 外层需要有足够的宽度,这样预览图和选择按钮才不会直接换行 -->
|
||||
<material-picker :limit="4" v-model="state.value2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card header="进阶用法" shadow="none" class="!border-none mt-4">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="flex m-4">
|
||||
<div class="mr-4">自定义选择器大小:</div>
|
||||
<material-picker size="60px" v-model="state.value4" />
|
||||
</div>
|
||||
<div class="flex m-4">
|
||||
<div class="mr-4">使用插槽:</div>
|
||||
<material-picker v-model="state.value5">
|
||||
<template #upload>
|
||||
<el-button>选择文件</el-button>
|
||||
</template>
|
||||
</material-picker>
|
||||
</div>
|
||||
<div class="flex m-4">
|
||||
<div class="mr-4">选出地址不带域名:</div>
|
||||
<material-picker :exclude-domain="true" v-model="state.value6" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex m-4 items-center">
|
||||
<div class="w-20 flex-none">带域名:</div>
|
||||
<el-input class="w-[500px]" :model-value="state.value5" />
|
||||
</div>
|
||||
<div class="flex m-4 items-center">
|
||||
<div class="w-20 flex-none">不带域名:</div>
|
||||
<el-input class="w-[500px]" :model-value="state.value6" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const state = reactive({
|
||||
value1: '',
|
||||
value2: [],
|
||||
value3: '',
|
||||
value4: '',
|
||||
value5: '',
|
||||
value6: ''
|
||||
})
|
||||
</script>
|
||||
64
admin/src/views/template/component/icon.vue
Normal file
64
admin/src/views/template/component/icon.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card header="element-plus图标" shadow="none" class="!border-none">
|
||||
<div class="flex items-center">
|
||||
<icon class="m-4" :size="24" name="el-icon-Search" />
|
||||
<icon class="m-4" :size="24" name="el-icon-Plus" />
|
||||
<icon class="m-4" :size="24" name="el-icon-FullScreen" />
|
||||
<icon class="m-4" :size="24" name="el-icon-Setting" />
|
||||
<icon class="m-4" :size="24" name="el-icon-Warning" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card header="本地图标" shadow="none" class="!border-none mt-4">
|
||||
<div class="flex items-center">
|
||||
<icon class="m-4" :size="24" name="local-icon-baoxian" />
|
||||
<icon class="m-4" :size="24" name="local-icon-youhui" />
|
||||
<icon class="m-4" :size="24" name="local-icon-daiyunying" />
|
||||
<icon class="m-4" :size="24" name="local-icon-diancanshezhi" />
|
||||
<icon class="m-4" :size="24" name="local-icon-dianzifapiao" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card header="图标选择器" shadow="none" class="!border-none mt-4">
|
||||
<div class="flex items-center">
|
||||
<icon-picker v-model="state.value" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card
|
||||
header="element-plus图标库大全(点击复制图标名称)"
|
||||
shadow="none"
|
||||
class="!border-none mt-4"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-wrap">
|
||||
<div v-for="item in getElementPlusIconNames()" :key="item" class="m-1">
|
||||
<el-button v-copy="item">
|
||||
<icon :name="item" :size="20" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card
|
||||
header="本地图标库大全(点击复制图标名称)"
|
||||
shadow="none"
|
||||
class="!border-none mt-4"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-wrap">
|
||||
<div v-for="item in getLocalIconNames()" :key="item" class="m-1">
|
||||
<el-button v-copy="item">
|
||||
<icon :name="item" :size="20" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Icon from '@/components/icon/index.vue'
|
||||
import { getElementPlusIconNames, getLocalIconNames } from '@/components/icon'
|
||||
const state = reactive({
|
||||
value: ''
|
||||
})
|
||||
</script>
|
||||
12
admin/src/views/template/component/link.vue
Normal file
12
admin/src/views/template/component/link.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card header="基础使用" shadow="none" class="!border-none">
|
||||
<link-picker v-model="state.value1" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const state = reactive({
|
||||
value1: {}
|
||||
})
|
||||
</script>
|
||||
9
admin/src/views/template/component/overflow.vue
Normal file
9
admin/src/views/template/component/overflow.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card header="基础使用" shadow="none" class="!border-none">
|
||||
<overflow-tooltip class="w-20 m-4" content="超出自动打点,悬浮弹窗显示全部内容" />
|
||||
<overflow-tooltip class="w-60 m-4" content="超出自动打点,悬浮弹窗显示全部内容" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
48
admin/src/views/template/component/popover_input.vue
Normal file
48
admin/src/views/template/component/popover_input.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card header="基础使用" shadow="none" class="!border-none">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="m-4">
|
||||
<popover-input @confirm="onConfirm">
|
||||
<template #default>
|
||||
<el-button> 点击输入 </el-button>
|
||||
</template>
|
||||
</popover-input>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<popover-input type="number" @confirm="onConfirm">
|
||||
<template #default>
|
||||
<el-button> 输入数字 </el-button>
|
||||
</template>
|
||||
</popover-input>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<popover-input size="small" @confirm="onConfirm">
|
||||
<template #default>
|
||||
<el-button> 调整大小 </el-button>
|
||||
</template>
|
||||
</popover-input>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<popover-input :limit="20" :show-limit="true" @confirm="onConfirm">
|
||||
<template #default>
|
||||
<el-button> 限制输入长度 </el-button>
|
||||
</template>
|
||||
</popover-input>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<popover-input value="默认值" @confirm="onConfirm">
|
||||
<template #default>
|
||||
<el-button> 默认值 </el-button>
|
||||
</template>
|
||||
</popover-input>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const onConfirm = (value: string) => {
|
||||
console.log(value)
|
||||
}
|
||||
</script>
|
||||
16
admin/src/views/template/component/rich_text.vue
Normal file
16
admin/src/views/template/component/rich_text.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card header="基础使用" shadow="none" class="!border-none">
|
||||
<editor v-model="state.value1" height="500px" />
|
||||
</el-card>
|
||||
<el-card header="简洁模式" shadow="none" class="!border-none mt-4">
|
||||
<editor v-model="state.value2" height="500px" mode="simple" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const state = reactive({
|
||||
value1: '',
|
||||
value2: ''
|
||||
})
|
||||
</script>
|
||||
65
admin/src/views/template/component/upload.vue
Normal file
65
admin/src/views/template/component/upload.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card header="基础使用" shadow="none" class="!border-none">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="m-4">
|
||||
<upload
|
||||
@change="onChange"
|
||||
@success="onSuccess"
|
||||
@error="onError"
|
||||
:show-progress="true"
|
||||
>
|
||||
<el-button type="primary">上传图片</el-button>
|
||||
</upload>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<upload
|
||||
type="video"
|
||||
@change="onChange"
|
||||
@success="onSuccess"
|
||||
@error="onError"
|
||||
:show-progress="true"
|
||||
>
|
||||
<el-button type="primary">上传视频</el-button>
|
||||
</upload>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<upload
|
||||
:multiple="false"
|
||||
@change="onChange"
|
||||
@success="onSuccess"
|
||||
@error="onError"
|
||||
:show-progress="true"
|
||||
>
|
||||
<el-button type="primary">取消多选</el-button>
|
||||
</upload>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<upload
|
||||
:limit="2"
|
||||
@change="onChange"
|
||||
@success="onSuccess"
|
||||
@error="onError"
|
||||
:show-progress="true"
|
||||
>
|
||||
<el-button type="primary">一次最多上传2张</el-button>
|
||||
</upload>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Upload from '@/components/upload/index.vue'
|
||||
const onChange = (file: any) => {
|
||||
console.log('上传文件的状态发生改变', file)
|
||||
}
|
||||
|
||||
const onSuccess = (file: any) => {
|
||||
console.log('上传文件成功', file)
|
||||
}
|
||||
|
||||
const onError = (file: any) => {
|
||||
console.log('上传文件失败', file)
|
||||
}
|
||||
</script>
|
||||
2
pc/.env.development.example
Normal file
2
pc/.env.development.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# 请求域名
|
||||
NUXT_API_URL=
|
||||
17
pc/.env.example
Normal file
17
pc/.env.example
Normal file
@@ -0,0 +1,17 @@
|
||||
# 版本号
|
||||
NUXT_VERSION=1.0
|
||||
|
||||
# 接口默认前缀
|
||||
NUXT_API_PREFIX=/api
|
||||
|
||||
# 客户端类型
|
||||
NUXT_CLIENT=4
|
||||
|
||||
# 基础路径
|
||||
NUXT_BASE_URL=/pc/
|
||||
|
||||
# 是否开启ssr,填些任意值开启
|
||||
NUXT_SSR=
|
||||
|
||||
# 端口号
|
||||
NITRO_PORT=3000
|
||||
3
pc/.env.production.example
Normal file
3
pc/.env.production.example
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
# 请求域名
|
||||
NUXT_API_URL=
|
||||
46
pc/.eslintrc.cjs
Normal file
46
pc/.eslintrc.cjs
Normal file
@@ -0,0 +1,46 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'plugin:nuxt/recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/eslint-config-prettier'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
rules: {
|
||||
'prettier/prettier': [
|
||||
'warn',
|
||||
{
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
printWidth: 80,
|
||||
proseWrap: 'preserve',
|
||||
bracketSameLine: false,
|
||||
endOfLine: 'auto',
|
||||
tabWidth: 4,
|
||||
useTabs: false,
|
||||
trailingComma: 'none'
|
||||
}
|
||||
],
|
||||
'no-useless-escape': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'no-undef': 'off',
|
||||
'vue/prefer-import-from-vue': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off'
|
||||
},
|
||||
globals: {
|
||||
module: 'readonly'
|
||||
}
|
||||
}
|
||||
11
pc/.gitignore
vendored
Normal file
11
pc/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
*.log*
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
.output
|
||||
.vite
|
||||
.env
|
||||
.env.development
|
||||
.env.production
|
||||
dist
|
||||
42
pc/README.md
Normal file
42
pc/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Nuxt 3 Minimal Starter
|
||||
|
||||
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install --shamefully-hoist
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on http://localhost:3000
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
|
||||
41
pc/api/account.ts
Normal file
41
pc/api/account.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// 登录
|
||||
export function mobileLogin(params: any) {
|
||||
return $request.post({
|
||||
url: '/login/mobileLogin',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function accountLogin(params: any) {
|
||||
return $request.post({
|
||||
url: '/login/accountLogin',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
//注册
|
||||
export function register(params: any) {
|
||||
return $request.post({
|
||||
url: '/login/register',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
//向微信请求code的链接
|
||||
export function getWxCodeUrl() {
|
||||
return $request.get({
|
||||
url: '/login/scanCodeUrl',
|
||||
params: {
|
||||
url: location.href
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function wxLogin(params: any) {
|
||||
return $request.post({ url: '/login/scanLogin', params })
|
||||
}
|
||||
|
||||
//忘记密码
|
||||
export function forgotPassword(params: Record<string, any>) {
|
||||
return $request.post({ url: '/login/forgotPassword', params })
|
||||
}
|
||||
19
pc/api/app.ts
Normal file
19
pc/api/app.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
//发送短信
|
||||
export function smsSend(params: any) {
|
||||
return $request.post({ url: '/index/sendSms', params })
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
export function getConfig() {
|
||||
return $request.get({ url: '/pc/getConfig' })
|
||||
}
|
||||
|
||||
// 获取协议
|
||||
export function getPolicy(params: any) {
|
||||
return $request.get({ url: '/index/policy', params })
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
export function uploadImage(params: any) {
|
||||
return $request.uploadFile({ url: '/upload/image' }, params)
|
||||
}
|
||||
57
pc/api/news.ts
Normal file
57
pc/api/news.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @description 获取文章分类
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getArticleCate() {
|
||||
return $request.get({ url: '/article/category' })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取文章列表
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getArticleList(params) {
|
||||
return $request.get({ url: '/article/list', params })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取资讯中心
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getArticleCenter() {
|
||||
return $request.get({ url: '/pc/articleCenter' })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 文章详情
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getArticleDetail(params) {
|
||||
return $request.get({ url: '/pc/articleDetail', params })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 加入收藏
|
||||
* @param { number } id
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function addCollect(params) {
|
||||
return $request.post({ url: '/article/collectAdd', params })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 取消收藏
|
||||
* @param { number } id
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function cancelCollect(params) {
|
||||
return $request.post({ url: '/article/collectCancel', params })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取收藏列表
|
||||
* @return { Promise }
|
||||
*/
|
||||
export function getCollect(params) {
|
||||
return $request.get({ url: '/article/collectList', params })
|
||||
}
|
||||
4
pc/api/shop.ts
Normal file
4
pc/api/shop.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
//首页数据
|
||||
export function getIndex() {
|
||||
return $request.get({ url: '/pc/index' })
|
||||
}
|
||||
26
pc/api/user.ts
Normal file
26
pc/api/user.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function getUserCenter(headers?: any) {
|
||||
return $request.get({ url: '/user/center', headers })
|
||||
}
|
||||
|
||||
// 个人信息
|
||||
export function getUserInfo() {
|
||||
return $request.get({ url: '/user/info' })
|
||||
}
|
||||
|
||||
// 个人编辑
|
||||
export function userEdit(params: any) {
|
||||
return $request.post({ url: '/user/edit', params })
|
||||
}
|
||||
|
||||
// 绑定手机
|
||||
export function userBindMobile(params: any, headers?: any) {
|
||||
return $request.post(
|
||||
{ url: '/user/bindMobile', params, headers },
|
||||
{ withToken: !headers?.token }
|
||||
)
|
||||
}
|
||||
|
||||
// 更改密码
|
||||
export function userChangePwd(params: any) {
|
||||
return $request.post({ url: '/user/changePwd', params })
|
||||
}
|
||||
35
pc/app.vue
Normal file
35
pc/app.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { ID_INJECTION_KEY, ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import { useAppStore } from './stores/app'
|
||||
provide(ID_INJECTION_KEY, {
|
||||
prefix: 100,
|
||||
current: 0
|
||||
})
|
||||
const config = {
|
||||
locale: zhCn
|
||||
}
|
||||
const appStore = useAppStore()
|
||||
const { pcTitle, pcIco, pcKeywords, pcDesc } = appStore.getWebsiteConfig
|
||||
useHead({
|
||||
title: pcTitle,
|
||||
meta: [
|
||||
{ name: 'description', content: pcDesc },
|
||||
{ name: 'keywords', content: pcKeywords }
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
href: pcIco
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ElConfigProvider v-bind="config">
|
||||
<NuxtLayout>
|
||||
<NuxtLoadingIndicator color="#4a5dff" :height="2" />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
BIN
pc/assets/images/empty_news.png
Normal file
BIN
pc/assets/images/empty_news.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
pc/assets/images/icon/icon_wx.png
Normal file
BIN
pc/assets/images/icon/icon_wx.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
pc/assets/images/placeholder.png
Normal file
BIN
pc/assets/images/placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
126
pc/assets/styles/element.scss
Normal file
126
pc/assets/styles/element.scss
Normal file
@@ -0,0 +1,126 @@
|
||||
@import 'element-plus/theme-chalk/index.css';
|
||||
|
||||
:root {
|
||||
// 弹窗居中
|
||||
.el-overlay-dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
position: static;
|
||||
|
||||
.el-dialog {
|
||||
--el-dialog-content-font-size: var(--el-font-size-base);
|
||||
--el-dialog-margin-top: 50px;
|
||||
max-width: calc(100vw - 30px);
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 5px;
|
||||
|
||||
&.body-padding .el-dialog__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
flex: 1;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
.el-dialog__header {
|
||||
font-size: var(--el-font-size-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-drawer {
|
||||
--el-drawer-padding-primary: 16px;
|
||||
&__header {
|
||||
margin-bottom: 0;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
&__title {
|
||||
@apply text-tx-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--el-table-header-text-color: var(--el-text-color-primary);
|
||||
--el-table-header-bg-color: var(--table-header-bg-color);
|
||||
font-size: var(--el-font-size-base);
|
||||
|
||||
thead {
|
||||
th {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-input-group__prepend {
|
||||
background-color: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
--el-checkbox-font-size: var(--el-font-size-base);
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
--el-messagebox-width: 350px;
|
||||
}
|
||||
.el-date-editor {
|
||||
--el-date-editor-datetimerange-width: 380px;
|
||||
.el-range-input {
|
||||
font-size: var(--el-font-size-small);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
--el-button-hover-link-text-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
.el-button--success {
|
||||
--el-button-hover-link-text-color: var(--el-color-success-light-3);
|
||||
}
|
||||
.el-button--info {
|
||||
--el-button-hover-link-text-color: var(--el-color-info-light-3);
|
||||
}
|
||||
.el-button--warning {
|
||||
--el-button-hover-link-text-color: var(--el-color-warning-light-3);
|
||||
}
|
||||
.el-button--danger {
|
||||
--el-button-hover-link-text-color: var(--el-color-danger-light-3);
|
||||
}
|
||||
.el-image__error {
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
.el-page-header {
|
||||
&__breadcrumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.el-card {
|
||||
--el-card-border-radius: 8px;
|
||||
}
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
// 防止被tailwindcss默认样式覆盖
|
||||
background-color: var(--el-button-bg-color, var(--el-color-white));
|
||||
|
||||
//覆盖el-button的点击样式
|
||||
&:focus {
|
||||
color: var(--el-button-text-color);
|
||||
border-color: var(--el-button-border-color);
|
||||
background-color: var(--el-button-bg-color);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--el-button-hover-text-color);
|
||||
border-color: var(--el-button-hover-border-color);
|
||||
background-color: var(--el-button-hover-bg-color);
|
||||
}
|
||||
}
|
||||
3
pc/assets/styles/index.scss
Normal file
3
pc/assets/styles/index.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@use 'element.scss';
|
||||
@use 'var.css';
|
||||
@use 'public.scss';
|
||||
56
pc/assets/styles/public.scss
Normal file
56
pc/assets/styles/public.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
body {
|
||||
@apply text-base text-tx-primary bg-page;
|
||||
min-width: 1200px;
|
||||
}
|
||||
body,
|
||||
html {
|
||||
// width: 100vw;
|
||||
}
|
||||
.form-tips {
|
||||
@apply text-tx-secondary text-xs leading-6 mt-1;
|
||||
}
|
||||
.el-button {
|
||||
background-color: var(--el-button-bg-color, var(--el-color-white));
|
||||
}
|
||||
.clearfix:after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.render-html {
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* NProgress */
|
||||
#nprogress .bar {
|
||||
@apply bg-primary #{!important};
|
||||
}
|
||||
56
pc/assets/styles/var.css
Normal file
56
pc/assets/styles/var.css
Normal file
@@ -0,0 +1,56 @@
|
||||
:root {
|
||||
--el-font-family: theme(fontFamily.sans);
|
||||
--el-color-primary: #4a5dff;
|
||||
--el-color-primary-dark-2: rgb(59, 74, 204);
|
||||
--el-color-primary-light-3: rgb(128, 142, 255);
|
||||
--el-color-primary-light-5: rgb(165, 174, 255);
|
||||
--el-color-primary-light-7: rgb(201, 206, 255);
|
||||
--el-color-primary-light-8: rgb(219, 223, 255);
|
||||
--el-color-primary-light-9: rgb(237, 239, 255);
|
||||
--el-font-weight-primary: 400;
|
||||
--el-menu-item-height: 46px;
|
||||
--el-menu-sub-item-height: var(--el-menu-item-height);
|
||||
--el-menu-icon-width: 18px;
|
||||
--aside-width: 200px;
|
||||
--header-height: 60px;
|
||||
--color-white: #ffffff;
|
||||
--table-header-bg-color: #f8f8f8;
|
||||
--el-font-size-extra-large: 18px;
|
||||
--el-menu-base-level-padding: 16px;
|
||||
--el-menu-level-padding: 26px;
|
||||
--el-font-size-large: 16px;
|
||||
--el-font-size-medium: 15px;
|
||||
--el-font-size-base: 14px;
|
||||
--el-font-size-small: 13px;
|
||||
--el-font-size-extra-small: 12px;
|
||||
|
||||
--el-bg-color: var(--color-white);
|
||||
--el-bg-color-page: #f7f7f7;
|
||||
--el-bg-color-overlay: #ffffff;
|
||||
--el-text-color-primary: #333333;
|
||||
--el-text-color-regular: #666666;
|
||||
--el-text-color-secondary: #999999;
|
||||
--el-text-color-placeholder: #a8abb2;
|
||||
--el-text-color-disabled: #c0c4cc;
|
||||
--el-border-color: #dcdfe6;
|
||||
--el-border-color-light: #e4e7ed;
|
||||
--el-border-color-lighter: #ebeef5;
|
||||
--el-border-color-extra-light: #f2f2f2;
|
||||
--el-border-color-dark: #d4d7de;
|
||||
--el-border-color-darker: #cdd0d6;
|
||||
--el-fill-color: #f0f2f5;
|
||||
--el-fill-color-light: #f5f7fa;
|
||||
--el-fill-color-lighter: #fafafa;
|
||||
--el-fill-color-extra-light: #fafcff;
|
||||
--el-fill-color-dark: #ebedf0;
|
||||
--el-fill-color-darker: #e6e8eb;
|
||||
--el-fill-color-blank: #ffffff;
|
||||
--el-mask-color: rgba(255, 255, 255, 0.9);
|
||||
--el-mask-color-extra-light: rgba(255, 255, 255, 0.3);
|
||||
-el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04),
|
||||
0px 8px 20px rgba(0, 0, 0, 0.08);
|
||||
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
|
||||
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
|
||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08),
|
||||
0px 12px 32px rgba(0, 0, 0, 0.12), 0px 8px 16px -8px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
73
pc/components/cropper-upload/index.vue
Normal file
73
pc/components/cropper-upload/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div>
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<slot />
|
||||
</ElUpload>
|
||||
<ElDialog
|
||||
v-model="state.cropperVisible"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="false"
|
||||
:width="600"
|
||||
@close="state.cropperVisible = false"
|
||||
>
|
||||
<div class="h-[400px]">
|
||||
<VueCropper
|
||||
ref="vueCropperRef"
|
||||
:img="state.imagePath"
|
||||
:autoCrop="true"
|
||||
:auto-crop-height="200"
|
||||
:auto-crop-width="200"
|
||||
output-type="png"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="handleConfirmCropper">
|
||||
确认裁剪
|
||||
</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElUpload, ElDialog, ElButton } from 'element-plus'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import { uploadImage } from '~~/api/app'
|
||||
const emit = defineEmits(['change'])
|
||||
const vueCropperRef = shallowRef()
|
||||
const uploadRef = shallowRef<InstanceType<typeof ElUpload>>()
|
||||
|
||||
const state = reactive({
|
||||
cropperVisible: false,
|
||||
imagePath: ''
|
||||
})
|
||||
|
||||
const handleChange = (rawFile) => {
|
||||
const URL = window.URL || window.webkitURL
|
||||
state.imagePath = URL.createObjectURL(rawFile.raw)
|
||||
state.cropperVisible = true
|
||||
}
|
||||
const handleConfirmCropper = () => {
|
||||
vueCropperRef.value?.getCropBlob(async (file) => {
|
||||
const fileName = `file.${file.type.split('/')[1]}`
|
||||
const imgFile = new window.File([file], fileName, {
|
||||
type: file.type
|
||||
})
|
||||
const data = await uploadImage({ file: imgFile })
|
||||
state.cropperVisible = false
|
||||
emit('change', data.path)
|
||||
uploadRef.value?.clearFiles()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
28
pc/components/icon/index.vue
Normal file
28
pc/components/icon/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<ElIcon v-bind="props" v-if="name.includes(EL_ICON_PREFIX)">
|
||||
<component :is="name" />
|
||||
</ElIcon>
|
||||
<span v-if="name.includes(LOCAL_ICON_PREFIX)" class="local-icon">
|
||||
<SvgIcon v-bind="props" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { EL_ICON_PREFIX, LOCAL_ICON_PREFIX } from '~~/plugins/icons'
|
||||
import SvgIcon from './svg-icon.vue'
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '14px'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
38
pc/components/icon/svg-icon.vue
Normal file
38
pc/components/icon/svg-icon.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" :style="styles">
|
||||
<use :xlink:href="symbolId" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { addUnit } from '@/utils/util'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const symbolId = computed(() => `#${props.name}`)
|
||||
const styles = computed<CSSProperties>(() => {
|
||||
return {
|
||||
width: addUnit(props.size),
|
||||
height: addUnit(props.size),
|
||||
color: props.color
|
||||
}
|
||||
})
|
||||
return { symbolId, styles }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
115
pc/components/information/card.vue
Normal file
115
pc/components/information/card.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-[8px]">
|
||||
<div class="flex items-center h-[60px] border-b border-br ml-5 pr-5">
|
||||
<div class="flex-1 flex min-w-0 mr-4 h-full">
|
||||
<span
|
||||
class="text-2xl truncate font-medium h-full border-b-2 border-tx-primary mt-[1px] flex items-center"
|
||||
>
|
||||
{{ header }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton class="button" link v-if="link">
|
||||
<NuxtLink :to="link" class="flex">
|
||||
更多
|
||||
<ElIcon><ArrowRight /></ElIcon>
|
||||
</NuxtLink>
|
||||
</ElButton>
|
||||
</div>
|
||||
<slot name="content" :data="data" v-if="data.length">
|
||||
<div class="px-5 pb-5">
|
||||
<template v-for="(item, index) in data" :key="item.id">
|
||||
<slot name="item" :item="item" :index="index">
|
||||
<InformationItems
|
||||
:index="index"
|
||||
:show-sort="showSort"
|
||||
:id="item.id"
|
||||
:title="item.title"
|
||||
:desc="item.intro"
|
||||
:click="item.visit"
|
||||
:author="item.author"
|
||||
:create-time="item.createTime"
|
||||
:image="item.image"
|
||||
:only-title="onlyTitle"
|
||||
:image-size="imageSize"
|
||||
:show-author="showAuthor"
|
||||
:show-desc="showDesc"
|
||||
:show-click="showClick"
|
||||
:border="border"
|
||||
:title-line="titleLine"
|
||||
:show-time="showTime"
|
||||
:source="source"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</div>
|
||||
</slot>
|
||||
<div v-else>
|
||||
<el-empty
|
||||
:image="empty_news"
|
||||
description="暂无资讯"
|
||||
:image-size="250"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElButton, ElIcon, ElEmpty } from 'element-plus'
|
||||
import empty_news from '@/assets/images/empty_news.png'
|
||||
import { ArrowRight } from '@element-plus/icons-vue'
|
||||
import { PropType } from 'vue'
|
||||
defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
data: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
onlyTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
titleLine: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
imageSize: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
showAuthor: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showDesc: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showTime: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showSort: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
168
pc/components/information/items.vue
Normal file
168
pc/components/information/items.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<NuxtLink :to="`/information/detail/${id}`">
|
||||
<div
|
||||
v-if="onlyTitle"
|
||||
class="before:w-[6px] mt-4 before:h-[6px] before:bg-primary before:block flex items-center before:rounded-[6px] before:mr-2.5 before:flex-none"
|
||||
>
|
||||
<slot name="title" :title="title">
|
||||
<span class="line-clamp-1 flex-1 font-medium">{{ title }}</span>
|
||||
</slot>
|
||||
<span class="text-tx-secondary ml-4" v-if="showTime">
|
||||
{{ createTime }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{
|
||||
'border-b border-br pb-4': border,
|
||||
'flex pt-4 items-center': !isHorizontal
|
||||
}"
|
||||
>
|
||||
<div class="flex relative">
|
||||
<ElImage
|
||||
v-if="image"
|
||||
class="flex-none"
|
||||
:class="{
|
||||
'mr-4': !isHorizontal
|
||||
}"
|
||||
:src="image"
|
||||
fit="cover"
|
||||
:style="getImageStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1"
|
||||
:class="{
|
||||
'p-2': isHorizontal
|
||||
}"
|
||||
>
|
||||
<slot name="title" :title="title">
|
||||
<div
|
||||
class="text-lg font-medium"
|
||||
:class="`line-clamp-${titleLine}`"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div
|
||||
v-if="showDesc && desc"
|
||||
class="text-tx-regular line-clamp-2 mt-4"
|
||||
>
|
||||
{{ desc }}
|
||||
</div>
|
||||
<div
|
||||
v-if="showAuthor || showTime || showClick"
|
||||
class="mt-5 text-tx-secondary flex items-center flex-wrap"
|
||||
>
|
||||
<span v-if="showAuthor && author">
|
||||
{{ author }} |
|
||||
</span>
|
||||
<span class="mr-5" v-if="showTime">{{ createTime }}</span>
|
||||
<div v-if="showClick" class="flex items-center">
|
||||
<ElIcon>
|
||||
<View />
|
||||
</ElIcon>
|
||||
<span> {{ click }}人浏览</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElImage, ElIcon } from 'element-plus'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
const props = defineProps({
|
||||
index: {
|
||||
type: Number
|
||||
},
|
||||
id: {
|
||||
type: Number
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
},
|
||||
image: {
|
||||
type: String
|
||||
},
|
||||
author: {
|
||||
type: String
|
||||
},
|
||||
click: {
|
||||
type: Number
|
||||
},
|
||||
createTime: {
|
||||
type: String
|
||||
},
|
||||
onlyTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isHorizontal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
titleLine: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
imageSize: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
showAuthor: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showDesc: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showTime: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showSort: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const getImageStyle = computed(() => {
|
||||
switch (props.imageSize) {
|
||||
case 'default':
|
||||
return {
|
||||
width: '180px',
|
||||
height: '135px'
|
||||
}
|
||||
case 'mini':
|
||||
return {
|
||||
width: '120px',
|
||||
height: '90px'
|
||||
}
|
||||
case 'large':
|
||||
return {
|
||||
width: '260px',
|
||||
height: '195px'
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
137
pc/components/popover-input/index.vue
Normal file
137
pc/components/popover-input/index.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div @mouseenter="inPopover = true" @mouseleave="inPopover = false">
|
||||
<el-popover
|
||||
placement="top"
|
||||
v-model:visible="visible"
|
||||
:width="width"
|
||||
trigger="contextmenu"
|
||||
class="popover-input"
|
||||
:teleported="teleported"
|
||||
:persistent="false"
|
||||
popper-class="!p-0"
|
||||
>
|
||||
<div class="flex p-3" @click.stop="">
|
||||
<div class="popover-input__input mr-[10px] flex-1">
|
||||
<el-select
|
||||
class="flex-1"
|
||||
:size="size"
|
||||
v-if="type == 'select'"
|
||||
v-model="inputValue"
|
||||
:teleported="teleported"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else
|
||||
v-model.trim="inputValue"
|
||||
:maxlength="limit"
|
||||
:show-word-limit="showLimit"
|
||||
:type="type"
|
||||
:size="size"
|
||||
clearable
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="popover-input__btns flex-none">
|
||||
<el-button link @click="close">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:size="size"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<div class="inline" @click.stop="handleOpen">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ElPopover, ElButton, ElSelect, ElOption, ElInput } from 'element-plus'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: '300px'
|
||||
},
|
||||
placeholder: String,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'default' | 'small' | 'large'>,
|
||||
default: 'default'
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
showLimit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
teleported: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['confirm'])
|
||||
const visible = ref(false)
|
||||
const inPopover = ref(false)
|
||||
const inputValue = ref()
|
||||
const handleConfirm = () => {
|
||||
close()
|
||||
emit('confirm', inputValue.value)
|
||||
}
|
||||
const handleOpen = () => {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
inputValue.value = value
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
useEventListener(document.documentElement, 'click', () => {
|
||||
if (inPopover.value) return
|
||||
close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
79
pc/components/verification-code/index.vue
Normal file
79
pc/components/verification-code/index.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<ElButton v-if="!isStart" @click="handlStart" link>
|
||||
{{ isRetry ? endText : startText }}
|
||||
</ElButton>
|
||||
<VueCountdown
|
||||
v-else
|
||||
ref="vueCountdownRef"
|
||||
:time="seconds * 1000"
|
||||
v-slot="{ totalSeconds }"
|
||||
@end="handleEnd"
|
||||
>
|
||||
{{ getChangeText(totalSeconds) }}
|
||||
</VueCountdown>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import VueCountdown from '@chenfengyuan/vue-countdown'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { ElButton } from 'element-plus'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VueCountdown,
|
||||
ElButton
|
||||
},
|
||||
props: {
|
||||
// 倒计时总秒数
|
||||
seconds: {
|
||||
type: Number,
|
||||
default: 60
|
||||
},
|
||||
// 尚未开始时提示
|
||||
startText: {
|
||||
type: String,
|
||||
default: '获取验证码'
|
||||
},
|
||||
// 正在倒计时中的提示
|
||||
changeText: {
|
||||
type: String,
|
||||
default: 'x秒重新获取'
|
||||
},
|
||||
// 倒计时结束时的提示
|
||||
endText: {
|
||||
type: String,
|
||||
default: '重新获取'
|
||||
}
|
||||
},
|
||||
emits: ['click-get'],
|
||||
setup(props, { emit }) {
|
||||
const isStart = ref(false)
|
||||
const isRetry = ref(false)
|
||||
const start = async () => {
|
||||
isStart.value = true
|
||||
}
|
||||
|
||||
const getChangeText = (second) => {
|
||||
return props.changeText.replace('x', second)
|
||||
}
|
||||
const handleEnd = () => {
|
||||
isStart.value = false
|
||||
isRetry.value = true
|
||||
}
|
||||
const handlStart = useThrottleFn(
|
||||
() => {
|
||||
emit('click-get')
|
||||
},
|
||||
1000,
|
||||
false
|
||||
)
|
||||
return {
|
||||
getChangeText,
|
||||
isStart,
|
||||
start,
|
||||
isRetry,
|
||||
handleEnd,
|
||||
handlStart
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
19
pc/composables/useLockFn.ts
Normal file
19
pc/composables/useLockFn.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function useLockFn(fn: (...args: any[]) => Promise<any>) {
|
||||
const isLock = ref(false)
|
||||
const lockFn = async (...args: any[]) => {
|
||||
if (isLock.value) return
|
||||
isLock.value = true
|
||||
try {
|
||||
const res = await fn(...args)
|
||||
isLock.value = false
|
||||
return res
|
||||
} catch (e) {
|
||||
isLock.value = false
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return {
|
||||
isLock,
|
||||
lockFn
|
||||
}
|
||||
}
|
||||
26
pc/composables/useMenu.ts
Normal file
26
pc/composables/useMenu.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NAVBAR, SIDEBAR } from '@/constants/menu'
|
||||
export default function useMenu() {
|
||||
const menu = useState(() => NAVBAR)
|
||||
const route = useRoute()
|
||||
const sidebar = computed(() => getSidebar(route.meta.module))
|
||||
const hasSidebar = computed(() => sidebar.value.length)
|
||||
return {
|
||||
menu,
|
||||
sidebar,
|
||||
hasSidebar
|
||||
}
|
||||
}
|
||||
|
||||
function getSidebar(module?: string): any[] {
|
||||
const queue: any[] = []
|
||||
SIDEBAR.forEach((item) => queue.push(item))
|
||||
while (queue.length) {
|
||||
const item = queue.shift()
|
||||
if (item.module && item.module == module) {
|
||||
return item.children
|
||||
}
|
||||
item.children &&
|
||||
item.children.forEach((child: any) => queue.push(child))
|
||||
}
|
||||
return []
|
||||
}
|
||||
55
pc/constants/menu.ts
Normal file
55
pc/constants/menu.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export const NAVBAR = [
|
||||
{
|
||||
name: '首页',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
name: '资讯中心',
|
||||
path: '/information',
|
||||
component: 'information'
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
path: '/mobile',
|
||||
component: 'mobile'
|
||||
},
|
||||
{
|
||||
name: '管理后台',
|
||||
path: '/admin',
|
||||
component: 'admin'
|
||||
}
|
||||
]
|
||||
|
||||
export const SIDEBAR = [
|
||||
{
|
||||
module: 'personal',
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
name: '个人中心',
|
||||
path: '/user',
|
||||
children: [
|
||||
{
|
||||
name: '个人信息',
|
||||
path: 'info'
|
||||
},
|
||||
{
|
||||
name: '我的收藏',
|
||||
path: 'collection'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: '账户设置',
|
||||
path: '/account',
|
||||
children: [
|
||||
{
|
||||
name: '账户安全',
|
||||
path: 'security'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
33
pc/enums/appEnums.ts
Normal file
33
pc/enums/appEnums.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
//菜单主题类型
|
||||
export enum ThemeEnum {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark'
|
||||
}
|
||||
|
||||
// 菜单类型
|
||||
export enum MenuEnum {
|
||||
CATALOGUE = 'M',
|
||||
MENU = 'C',
|
||||
BUTTON = 'A'
|
||||
}
|
||||
|
||||
// 屏幕
|
||||
export enum ScreenEnum {
|
||||
SM = 640,
|
||||
MD = 768,
|
||||
LG = 1024,
|
||||
XL = 1280,
|
||||
'2XL' = 1536
|
||||
}
|
||||
|
||||
export enum SMSEnum {
|
||||
LOGIN = 101,
|
||||
BIND_MOBILE = 102,
|
||||
CHANGE_MOBILE = 103,
|
||||
FIND_PASSWORD = 104
|
||||
}
|
||||
|
||||
export enum PolicyAgreementEnum {
|
||||
SERVICE = 'service',
|
||||
PRIVACY = 'privacy'
|
||||
}
|
||||
8
pc/enums/cacheEnums.ts
Normal file
8
pc/enums/cacheEnums.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// 本地缓冲key
|
||||
|
||||
//token
|
||||
export const TOKEN_KEY = 'token'
|
||||
//账号
|
||||
export const ACCOUNT_KEY = 'account'
|
||||
//设置
|
||||
export const SETTING_KEY = 'setting'
|
||||
7
pc/enums/pageEnum.ts
Normal file
7
pc/enums/pageEnum.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum PageEnum {
|
||||
//登录页面
|
||||
LOGIN = '/login',
|
||||
//无权限页面
|
||||
ERROR_403 = '/403',
|
||||
INDEX = '/'
|
||||
}
|
||||
28
pc/enums/requestEnums.ts
Normal file
28
pc/enums/requestEnums.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export enum ContentTypeEnum {
|
||||
// json
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
// form-data 上传资源(图片,视频)
|
||||
FORM_DATA = 'multipart/form-data'
|
||||
}
|
||||
|
||||
export enum RequestMethodsEnum {
|
||||
GET = 'GET',
|
||||
POST = 'POST'
|
||||
}
|
||||
|
||||
export enum RequestCodeEnum {
|
||||
SUCCESS = 200, //成功
|
||||
FAILED = 300, // 失败
|
||||
PARAMS_VALID_ERROR = 310, //参数校验错误
|
||||
PARAMS_TYPE_ERROR = 311, //参数类型错误
|
||||
REQUEST_METHOD_ERROR = 312, //请求方法错误
|
||||
ASSERT_ARGUMENT_ERROR = 313, //断言参数错误
|
||||
ASSERT_MYBATIS_ERROR = 314, //断言mybatis错误
|
||||
LOGIN_ACCOUNT_ERROR = 330, //登陆账号或密码错误
|
||||
LOGIN_DISABLE_ERROR = 331, //登陆账号已被禁用
|
||||
TOKEN_EMPTY = 332, // TOKEN参数为空
|
||||
TOKEN_INVALID = 333, // TOKEN参数无效
|
||||
NO_PERMISSTION = 403, //无相关权限
|
||||
REQUEST_404_ERROR = 404, //请求接口不存在
|
||||
SYSTEM_ERROR = 500 //系统错误
|
||||
}
|
||||
5
pc/global.d.ts
vendored
Normal file
5
pc/global.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
import { Request } from '@/utils/http/request'
|
||||
declare global {
|
||||
const $request: Request
|
||||
}
|
||||
6
pc/layouts/blank.vue
Normal file
6
pc/layouts/blank.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<section class="layout-blank">
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
113
pc/layouts/components/account/bind-mobile.vue
Normal file
113
pc/layouts/components/account/bind-mobile.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-4xl">
|
||||
{{ hasMobile ? '更换手机号' : '绑定手机号' }}
|
||||
</span>
|
||||
</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<ElFormItem prop="mobile">
|
||||
<ElInput
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="code">
|
||||
<ElInput v-model="formData.code" placeholder="请输入验证码">
|
||||
<template #suffix>
|
||||
<div
|
||||
class="flex justify-center leading-5 w-[90px] pl-2.5 border-l border-br"
|
||||
>
|
||||
<VerificationCode
|
||||
ref="verificationCodeRef"
|
||||
@click-get="sendSms"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem class="mt-[60px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
@click="handleConfirmLock"
|
||||
:loading="isLock"
|
||||
>
|
||||
确认
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { smsSend } from '~~/api/app'
|
||||
import { userBindMobile } from '~~/api/user'
|
||||
import { SMSEnum } from '~~/enums/appEnums'
|
||||
import { useAccount } from './useAccount'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
const { toggleShowPopup } = useAccount()
|
||||
const userStore = useUserStore()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const verificationCodeRef = shallowRef()
|
||||
const formRules: FormRules = {
|
||||
mobile: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入手机号码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const hasMobile = computed(() => !!userStore.userInfo.mobile)
|
||||
|
||||
const formData = reactive({
|
||||
type: hasMobile.value ? 'change' : 'bind',
|
||||
mobile: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const sendSms = async () => {
|
||||
await formRef.value?.validateField(['mobile'])
|
||||
await smsSend({
|
||||
scene: hasMobile.value ? SMSEnum.CHANGE_MOBILE : SMSEnum.BIND_MOBILE,
|
||||
mobile: formData.mobile
|
||||
})
|
||||
verificationCodeRef.value?.start()
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
if (userStore.isLogin) {
|
||||
await userBindMobile(formData)
|
||||
} else {
|
||||
await userBindMobile(formData, { token: userStore.temToken })
|
||||
userStore.login(userStore.temToken)
|
||||
await userStore.getUser()
|
||||
}
|
||||
toggleShowPopup(false)
|
||||
}
|
||||
const { lockFn: handleConfirmLock, isLock } = useLockFn(handleConfirm)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
164
pc/layouts/components/account/forgot-pwd.vue
Normal file
164
pc/layouts/components/account/forgot-pwd.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-4xl">忘记登录密码</span>
|
||||
<ElButton
|
||||
type="primary"
|
||||
link
|
||||
@click="setPopupType(PopupTypeEnum.LOGIN)"
|
||||
v-if="!userStore.isLogin"
|
||||
>
|
||||
返回登录
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<ElFormItem prop="mobile">
|
||||
<ElInput
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="code">
|
||||
<ElInput v-model="formData.code" placeholder="请输入验证码">
|
||||
<template #suffix>
|
||||
<div
|
||||
class="flex justify-center leading-5 w-[90px] pl-2.5 border-l border-br"
|
||||
>
|
||||
<VerificationCode
|
||||
ref="verificationCodeRef"
|
||||
@click-get="sendSms"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
placeholder="请输入6-20位数字+字母或符号组合"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="passwordConfirm">
|
||||
<ElInput
|
||||
v-model="formData.passwordConfirm"
|
||||
placeholder="请再次输入密码"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem class="mt-[60px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
@click="handleConfirmLock"
|
||||
:loading="isLock"
|
||||
>
|
||||
确认
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { smsSend } from '~~/api/app'
|
||||
import { forgotPassword } from '~~/api/account'
|
||||
import { SMSEnum } from '~~/enums/appEnums'
|
||||
import { useUserStore } from '~~/stores/user'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const userStore = useUserStore()
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const verificationCodeRef = shallowRef()
|
||||
const formRules: FormRules = {
|
||||
mobile: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入手机号码',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 12,
|
||||
message: '账号长度应为3-12',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入6-20位数字+字母或符号组合',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '密码长度应为6-20',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
passwordConfirm: [
|
||||
{
|
||||
validator(rule: any, value: any, callback: any) {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const formData = reactive({
|
||||
mobile: '',
|
||||
password: '',
|
||||
code: '',
|
||||
passwordConfirm: ''
|
||||
})
|
||||
|
||||
const sendSms = async () => {
|
||||
await formRef.value?.validateField(['mobile'])
|
||||
await smsSend({
|
||||
scene: SMSEnum.FIND_PASSWORD,
|
||||
mobile: formData.mobile
|
||||
})
|
||||
verificationCodeRef.value?.start()
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
await forgotPassword(formData)
|
||||
feedback.msgSuccess('操作成功')
|
||||
userStore.logout()
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
}
|
||||
const { lockFn: handleConfirmLock, isLock } = useLockFn(handleConfirm)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
36
pc/layouts/components/account/index.vue
Normal file
36
pc/layouts/components/account/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="account" v-if="showPopup">
|
||||
<ClientOnly>
|
||||
<ElDialog
|
||||
v-model="showPopup"
|
||||
:width="400"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="px-5 text-tx-primary">
|
||||
<Login v-show="popupType == PopupTypeEnum.LOGIN" />
|
||||
<Register v-show="popupType == PopupTypeEnum.REGISTER" />
|
||||
<ForgotPwd v-show="popupType == PopupTypeEnum.FORGOT_PWD" />
|
||||
<BindMobile
|
||||
v-show="popupType == PopupTypeEnum.BIND_MOBILE"
|
||||
/>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElDialog } from 'element-plus'
|
||||
import Login from './login.vue'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import Register from './register.vue'
|
||||
import ForgotPwd from './forgot-pwd.vue'
|
||||
import BindMobile from './bind-mobile.vue'
|
||||
import { useUserStore } from '~~/stores/user'
|
||||
const { popupType, showPopup } = useAccount()
|
||||
const userStore = useUserStore()
|
||||
watch(showPopup, (value) => {
|
||||
if (!value) userStore.temToken = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
331
pc/layouts/components/account/login.vue
Normal file
331
pc/layouts/components/account/login.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="text-4xl">欢迎登录</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<template
|
||||
v-if="isAccountLogin && includeLoginWay(LoginWayEnum.ACCOUNT)"
|
||||
>
|
||||
<ElFormItem prop="account">
|
||||
<ElInput
|
||||
v-model="formData.account"
|
||||
placeholder="请输入账号/手机号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<template
|
||||
v-if="isMobileLogin && includeLoginWay(LoginWayEnum.MOBILE)"
|
||||
>
|
||||
<ElFormItem prop="account">
|
||||
<ElInput
|
||||
v-model="formData.account"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="code">
|
||||
<ElInput v-model="formData.code" placeholder="请输入验证码">
|
||||
<template #suffix>
|
||||
<div
|
||||
class="flex justify-center leading-5 w-[90px] pl-2.5 border-l border-br"
|
||||
>
|
||||
<VerificationCode
|
||||
ref="verificationCodeRef"
|
||||
@click-get="sendSms"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<div class="flex">
|
||||
<div class="flex-1">
|
||||
<ElButton
|
||||
v-if="
|
||||
isAccountLogin &&
|
||||
includeLoginWay(LoginWayEnum.MOBILE)
|
||||
"
|
||||
type="primary"
|
||||
link
|
||||
@click="changeLoginWay"
|
||||
>
|
||||
手机验证码登录
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="
|
||||
isMobileLogin &&
|
||||
includeLoginWay(LoginWayEnum.ACCOUNT)
|
||||
"
|
||||
type="primary"
|
||||
link
|
||||
@click="changeLoginWay"
|
||||
>
|
||||
账号密码登录
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElButton
|
||||
v-if="isAccountLogin"
|
||||
link
|
||||
@click="setPopupType(PopupTypeEnum.FORGOT_PWD)"
|
||||
>
|
||||
忘记密码?
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElFormItem class="mt-[30px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
:loading="isLock"
|
||||
@click="loginLock"
|
||||
>
|
||||
登录
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<div class="mt-[40px]" v-if="isOpenOtherAuth">
|
||||
<ElDivider>
|
||||
<span class="text-tx-secondary font-normal">
|
||||
第三方登录
|
||||
</span>
|
||||
</ElDivider>
|
||||
<div class="flex justify-center">
|
||||
<ElButton link @click="getWxCodeLock" v-if="inWxAuth">
|
||||
<img
|
||||
class="w-[48px] h-[48px]"
|
||||
src="@/assets/images/icon/icon_wx.png"
|
||||
/>
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mb-[-15px] mx-[-40px] mt-[30px] bg-primary-light-9 rounded-b-md px-[15px] flex leading-10"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<ElCheckbox v-if="isOpenAgreement" v-model="isAgreement">
|
||||
<span class="text-tx-secondary text-sm">
|
||||
已阅读并同意
|
||||
<NuxtLink
|
||||
:to="`/policy/${PolicyAgreementEnum.SERVICE}`"
|
||||
custom
|
||||
v-slot="{ href }"
|
||||
>
|
||||
<a
|
||||
class="text-tx-primary"
|
||||
:href="href"
|
||||
target="_blank"
|
||||
>
|
||||
《服务协议》
|
||||
</a>
|
||||
</NuxtLink>
|
||||
和
|
||||
<NuxtLink
|
||||
class="text-tx-primary"
|
||||
:to="`/policy/${PolicyAgreementEnum.PRIVACY}`"
|
||||
custom
|
||||
v-slot="{ href }"
|
||||
>
|
||||
<a
|
||||
class="text-tx-primary"
|
||||
:href="href"
|
||||
target="_blank"
|
||||
>
|
||||
《隐私政策》
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</ElCheckbox>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
@click="setPopupType(PopupTypeEnum.REGISTER)"
|
||||
>
|
||||
<span class="text-sm">注册账号</span>
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
ElDivider,
|
||||
ElCheckbox,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import { getWxCodeUrl, mobileLogin, accountLogin } from '@/api/account'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { smsSend } from '~~/api/app'
|
||||
import { PolicyAgreementEnum, SMSEnum } from '~~/enums/appEnums'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
enum LoginWayEnum {
|
||||
ACCOUNT = 1,
|
||||
MOBILE = 2
|
||||
}
|
||||
const isAgreement = ref(false)
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formRules: FormRules = {
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
validator(rule: any, value: any, callback: any) {
|
||||
if (value === '') {
|
||||
callback(
|
||||
new Error(
|
||||
formData.scene == LoginWayEnum.ACCOUNT
|
||||
? '请输入账号/手机号'
|
||||
: '请输入手机号'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const formData = reactive({
|
||||
code: '',
|
||||
account: '',
|
||||
password: '',
|
||||
scene: 0
|
||||
})
|
||||
const isAccountLogin = computed(() => formData.scene == LoginWayEnum.ACCOUNT)
|
||||
const isMobileLogin = computed(() => formData.scene == LoginWayEnum.MOBILE)
|
||||
const includeLoginWay = (way: LoginWayEnum) =>
|
||||
appStore.getLoginConfig.loginWay?.includes(way)
|
||||
|
||||
const inWxAuth = computed(() => {
|
||||
return appStore.getLoginConfig.autoLoginAuth.includes(2)
|
||||
})
|
||||
|
||||
const isOpenAgreement = computed(
|
||||
() => appStore.getLoginConfig.openAgreement == 1
|
||||
)
|
||||
const isOpenOtherAuth = computed(
|
||||
() => appStore.getLoginConfig.openOtherAuth == 1
|
||||
)
|
||||
const isForceBindMobile = computed(
|
||||
() => appStore.getLoginConfig.forceBindMobile == 1
|
||||
)
|
||||
const changeLoginWay = () => {
|
||||
if (formData.scene == LoginWayEnum.ACCOUNT) {
|
||||
formData.scene = LoginWayEnum.MOBILE
|
||||
} else {
|
||||
formData.scene = LoginWayEnum.ACCOUNT
|
||||
}
|
||||
}
|
||||
const verificationCodeRef = shallowRef()
|
||||
const sendSms = async () => {
|
||||
await formRef.value?.validateField(['account'])
|
||||
await smsSend({
|
||||
scene: SMSEnum.LOGIN,
|
||||
mobile: formData.account
|
||||
})
|
||||
|
||||
verificationCodeRef.value?.start()
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
await formRef.value?.validate()
|
||||
const params: any = {}
|
||||
if (isAccountLogin.value) {
|
||||
params.username = formData.account
|
||||
params.password = formData.password
|
||||
}
|
||||
if (isMobileLogin.value) {
|
||||
params.mobile = formData.account
|
||||
params.code = formData.code
|
||||
}
|
||||
let data
|
||||
switch (formData.scene) {
|
||||
case LoginWayEnum.ACCOUNT:
|
||||
data = await accountLogin(params)
|
||||
break
|
||||
case LoginWayEnum.MOBILE:
|
||||
data = await mobileLogin(params)
|
||||
|
||||
break
|
||||
}
|
||||
if (!data) return
|
||||
if (isForceBindMobile.value && !data.isBindMobile) {
|
||||
userStore.temToken = data.token
|
||||
setPopupType(PopupTypeEnum.BIND_MOBILE)
|
||||
return
|
||||
}
|
||||
userStore.login(data.token)
|
||||
await userStore.getUser()
|
||||
toggleShowPopup(false)
|
||||
}
|
||||
const { lockFn: handleLoginLock, isLock } = useLockFn(handleLogin)
|
||||
const agreementConfirm = async () => {
|
||||
if (isAgreement.value) {
|
||||
return
|
||||
}
|
||||
await feedback.confirm('确认已阅读并同意《服务协议》和《隐私政策》')
|
||||
isAgreement.value = true
|
||||
}
|
||||
const loginLock = async () => {
|
||||
await agreementConfirm()
|
||||
await handleLoginLock()
|
||||
}
|
||||
|
||||
const getWxCode = async () => {
|
||||
await agreementConfirm()
|
||||
const { url } = await getWxCodeUrl()
|
||||
window.location.href = url
|
||||
}
|
||||
const { lockFn: getWxCodeLock } = useLockFn(getWxCode)
|
||||
watch(
|
||||
() => appStore.getLoginConfig,
|
||||
(value) => {
|
||||
const { loginWay } = value
|
||||
if (loginWay && loginWay.length) {
|
||||
formData.scene = loginWay.at(0)
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
126
pc/layouts/components/account/register.vue
Normal file
126
pc/layouts/components/account/register.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-4xl">注册账号</span>
|
||||
<ElButton
|
||||
type="primary"
|
||||
link
|
||||
@click="setPopupType(PopupTypeEnum.LOGIN)"
|
||||
>
|
||||
返回登录
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
class="mt-[35px]"
|
||||
size="large"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
v-model="formData.username"
|
||||
placeholder="请输入创建的账号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入6-20位数字+字母或符号组合"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="passwordConfirm">
|
||||
<ElInput
|
||||
v-model="formData.passwordConfirm"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem class="mt-[60px]">
|
||||
<ElButton
|
||||
class="w-full"
|
||||
type="primary"
|
||||
:loading="isLock"
|
||||
@click="handleConfirmLock"
|
||||
>
|
||||
注册
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElButton,
|
||||
FormInstance,
|
||||
FormRules
|
||||
} from 'element-plus'
|
||||
import { register } from '~~/api/account'
|
||||
import feedback from '~~/utils/feedback'
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
const { setPopupType } = useAccount()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formRules: FormRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入创建的账号',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 12,
|
||||
message: '账号长度应为3-12',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入6-20位数字+字母或符号组合',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '密码长度应为6-20',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
],
|
||||
passwordConfirm: [
|
||||
{
|
||||
validator(rule: any, value: any, callback: any) {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
passwordConfirm: ''
|
||||
})
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await formRef.value?.validate()
|
||||
await register(formData)
|
||||
feedback.msgSuccess('注册成功')
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
}
|
||||
const { lockFn: handleConfirmLock, isLock } = useLockFn(handleConfirm)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
17
pc/layouts/components/account/to-login.vue
Normal file
17
pc/layouts/components/account/to-login.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-tx-regular mb-4">您还未登录,请先登录</div>
|
||||
<ElButton @click="toLogin">登录</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAccount, PopupTypeEnum } from './useAccount'
|
||||
import { ElButton } from 'element-plus'
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
const toLogin = () => {
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
toggleShowPopup(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
23
pc/layouts/components/account/useAccount.ts
Normal file
23
pc/layouts/components/account/useAccount.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export enum PopupTypeEnum {
|
||||
LOGIN,
|
||||
FORGOT_PWD,
|
||||
REGISTER,
|
||||
BIND_MOBILE
|
||||
}
|
||||
|
||||
export const useAccount = () => {
|
||||
const popupType = useState<PopupTypeEnum>(() => PopupTypeEnum.LOGIN)
|
||||
const setPopupType = (type: PopupTypeEnum = PopupTypeEnum.LOGIN) => {
|
||||
popupType.value = type
|
||||
}
|
||||
const showPopup = useState(() => false)
|
||||
const toggleShowPopup = (toggle: boolean) => {
|
||||
showPopup.value = toggle ?? !showPopup.value
|
||||
}
|
||||
return {
|
||||
popupType,
|
||||
setPopupType,
|
||||
showPopup,
|
||||
toggleShowPopup
|
||||
}
|
||||
}
|
||||
35
pc/layouts/components/footer/index.vue
Normal file
35
pc/layouts/components/footer/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<footer class="layout-footer text-center bg-[#222222] py-[30px]">
|
||||
<div class="text-[#bebebe]">
|
||||
<!-- <NuxtLink> 关于我们 </NuxtLink>
|
||||
| -->
|
||||
<NuxtLink :to="`/policy/${PolicyAgreementEnum.SERVICE}`">
|
||||
用户协议
|
||||
</NuxtLink>
|
||||
|
|
||||
<NuxtLink :to="`/policy/${PolicyAgreementEnum.PRIVACY}`">
|
||||
隐私政策
|
||||
</NuxtLink>
|
||||
|
|
||||
<NuxtLink to="/user/info"> 会员中心 </NuxtLink>
|
||||
</div>
|
||||
<div class="mt-4 text-tx-secondary">
|
||||
<a
|
||||
class="mx-1 hover:underline"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
v-for="item in appStore.getCopyrightConfig"
|
||||
:key="item.link"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { PolicyAgreementEnum } from '@/enums/appEnums'
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
24
pc/layouts/components/header/admin.vue
Normal file
24
pc/layouts/components/header/admin.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<NuxtLink :to="appStore.getAdminUrl" target="_blank">
|
||||
<ElMenuItem :index="menuItem.path">
|
||||
<template #title>
|
||||
<span>
|
||||
{{ menuItem.name }}
|
||||
</span>
|
||||
</template>
|
||||
</ElMenuItem>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElMenuItem } from 'element-plus'
|
||||
import { useAppStore } from '~~/stores/app'
|
||||
defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
44
pc/layouts/components/header/index.vue
Normal file
44
pc/layouts/components/header/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<header class="layout-header text-white bg-primary">
|
||||
<div class="header-contain">
|
||||
<Logo class="flex-none mr-4" />
|
||||
<Navbar class="w-[600px]" />
|
||||
<div class="flex-1"></div>
|
||||
<Search class="mr-[40px] flex-none" />
|
||||
<User class="flex-none" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import User from './user.vue'
|
||||
import Search from './search.vue'
|
||||
import Logo from './logo.vue'
|
||||
import Navbar from './navbar.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header {
|
||||
height: var(--header-height);
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1999;
|
||||
.header-contain {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
.navbar {
|
||||
--el-menu-item-font-size: var(--el-font-size-large);
|
||||
--el-menu-bg-color: var(--el-color-primary);
|
||||
--el-menu-active-color: var(--color-white);
|
||||
--el-menu-text-color: var(--color-white);
|
||||
--el-menu-item-hover-fill: var(--el-color-primary);
|
||||
--el-menu-hover-text-color: var(--color-white);
|
||||
--el-menu-hover-bg-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
pc/layouts/components/header/information.vue
Normal file
47
pc/layouts/components/header/information.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<el-dropdown :max-height="200" :disabled="!hasData">
|
||||
<span class="flex items-center text-white">
|
||||
<MenuItem :menu-item="menuItem" :route-path="menuItem.path" />
|
||||
<span class="ml-[-10px]" v-if="hasData">
|
||||
<Icon name="el-icon-ArrowDown" />
|
||||
</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<NuxtLink
|
||||
:to="{
|
||||
path: '/information/search',
|
||||
query: {
|
||||
cid: item.id,
|
||||
name: item.name
|
||||
}
|
||||
}"
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
>
|
||||
<el-dropdown-item> {{ item.name }} </el-dropdown-item>
|
||||
</NuxtLink>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus'
|
||||
import { getArticleCate } from '~~/api/news'
|
||||
import MenuItem from '../menu/menu-item.vue'
|
||||
defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const { data } = await useAsyncData(() => getArticleCate())
|
||||
const hasData = computed(() => {
|
||||
return data.value && data.value.length
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
11
pc/layouts/components/header/logo.vue
Normal file
11
pc/layouts/components/header/logo.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<NuxtLink v-if="appStore.getWebsiteConfig.pcLogo" class="flex" to="/">
|
||||
<img :src="appStore.getWebsiteConfig.pcLogo" class="h-[26px]" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '~~/stores/app'
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
58
pc/layouts/components/header/mobile.vue
Normal file
58
pc/layouts/components/header/mobile.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<ElMenuItem :index="menuItem.path" @click="showMobilePopup = true">
|
||||
<template #title>
|
||||
<span>
|
||||
{{ menuItem.name }}
|
||||
</span>
|
||||
</template>
|
||||
</ElMenuItem>
|
||||
<ClientOnly>
|
||||
<ElDialog
|
||||
v-model="showMobilePopup"
|
||||
@close="showMobilePopup = false"
|
||||
:width="700"
|
||||
>
|
||||
<div class="text-center text-tx-primary">
|
||||
<div class="text-4xl font-medium">移动端演示</div>
|
||||
<div class="flex my-[40px] justify-around">
|
||||
<div v-if="oa">
|
||||
<img :src="oa" class="w-[180px] h-[180px]" alt="" />
|
||||
<div class="mt-2.5">微信公众号演示</div>
|
||||
</div>
|
||||
<div v-if="mnp">
|
||||
<img
|
||||
:src="mnp"
|
||||
class="w-[180px] h-[180px]"
|
||||
alt=""
|
||||
/>
|
||||
<div class="mt-2.5">微信小程序演示</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!mnp && !oa"
|
||||
class="w-[180px] h-[180px] flex items-center justify-center"
|
||||
>
|
||||
暂无演示
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElMenuItem, ElDialog } from 'element-plus'
|
||||
import { useAppStore } from '~~/stores/app'
|
||||
defineProps({
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
const mnp = computed(() => appStore.getQrcodeConfig.mnp)
|
||||
const oa = computed(() => appStore.getQrcodeConfig.oa)
|
||||
const showMobilePopup = ref(false)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
55
pc/layouts/components/header/navbar.vue
Normal file
55
pc/layouts/components/header/navbar.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<nav>
|
||||
<Menu
|
||||
class="navbar"
|
||||
:menu="menu"
|
||||
:default-active="activeMenu"
|
||||
mode="horizontal"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<MenuItem
|
||||
v-if="!item.component"
|
||||
:menu-item="item"
|
||||
:route-path="item.path"
|
||||
/>
|
||||
<div v-else>
|
||||
<template v-if="item.component == 'information'">
|
||||
<Information :menu-item="item" />
|
||||
</template>
|
||||
<template v-if="item.component == 'mobile'">
|
||||
<Mobile :menu-item="item" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</nav>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Menu from '../menu/index.vue'
|
||||
import MenuItem from '../menu/menu-item.vue'
|
||||
import Admin from './admin.vue'
|
||||
import Information from './information.vue'
|
||||
import Mobile from './mobile.vue'
|
||||
const route = useRoute()
|
||||
const activeMenu = computed<string>(() => route.path)
|
||||
const { menu } = useMenu()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
--el-menu-item-font-size: var(--el-font-size-large);
|
||||
--el-menu-bg-color: var(--el-color-primary);
|
||||
--el-menu-active-color: var(--color-white);
|
||||
--el-menu-text-color: var(--color-white);
|
||||
--el-menu-item-hover-fill: var(--el-color-primary);
|
||||
--el-menu-hover-text-color: var(--color-white);
|
||||
--el-menu-hover-bg-color: var(--el-color-primary);
|
||||
:deep() {
|
||||
& > .el-sub-menu {
|
||||
.el-sub-menu__title:hover {
|
||||
background-color: var(--el-menu-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
pc/layouts/components/header/search.vue
Normal file
50
pc/layouts/components/header/search.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="w-[250px] search">
|
||||
<ElInput
|
||||
v-model.trim="searchKeyword"
|
||||
placeholder="请输入关键词"
|
||||
:suffix-icon="Search"
|
||||
@keyup.enter="handleToSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElInput } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const searchKeyword = ref()
|
||||
const handleToSearch = () => {
|
||||
if (!searchKeyword.value) return feedback.msgError('请输入关键词')
|
||||
router.push({
|
||||
path: '/information/search',
|
||||
query: {
|
||||
keywords: searchKeyword.value
|
||||
}
|
||||
})
|
||||
}
|
||||
watch(
|
||||
route,
|
||||
(routeNew) => {
|
||||
if (routeNew.path == '/information/search') {
|
||||
searchKeyword.value = routeNew.query.keywords
|
||||
} else {
|
||||
searchKeyword.value = ''
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
:deep(.el-input) {
|
||||
.el-input__wrapper {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
65
pc/layouts/components/header/user.vue
Normal file
65
pc/layouts/components/header/user.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<ElDropdown v-if="userStore.isLogin" @command="handleCommand">
|
||||
<div class="flex items-center">
|
||||
<ElAvatar :size="25" :src="userStore.userInfo.avatar" />
|
||||
<div class="ml-1 text-white text-lg flex">
|
||||
<span class="mr-2">个人中心</span>
|
||||
<ElIcon><ArrowDown /></ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<NuxtLink to="/user/info">
|
||||
<ElDropdownItem command="user">个人信息</ElDropdownItem>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/user/collection">
|
||||
<ElDropdownItem command="collect">
|
||||
我的收藏
|
||||
</ElDropdownItem>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/account/security">
|
||||
<ElDropdownItem command="account">
|
||||
账号安全
|
||||
</ElDropdownItem>
|
||||
</NuxtLink>
|
||||
<ElDropdownItem command="logout">退出登录</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
|
||||
<div v-else class="cursor-pointer text-lg" @click="handleToLogin">
|
||||
登录/注册
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElAvatar,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
ElIcon
|
||||
} from 'element-plus'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { PopupTypeEnum, useAccount } from '../account/useAccount'
|
||||
import feedback from '~~/utils/feedback'
|
||||
const { setPopupType, toggleShowPopup } = useAccount()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const handleToLogin = () => {
|
||||
setPopupType(PopupTypeEnum.LOGIN)
|
||||
toggleShowPopup(true)
|
||||
}
|
||||
|
||||
const handleCommand = async (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
await feedback.confirm('确定退出登录吗?')
|
||||
userStore.logout()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user