Merge branch 'test'

This commit is contained in:
2023-11-29 15:33:51 +08:00
103 changed files with 14556 additions and 4404 deletions

View File

@@ -1,23 +1,26 @@
module.exports = {
types: [
{ value: 'feat', name:'feat: 新增功能' },
{ value: 'fix', name:'fix: 修复bug' },
{ value: 'docs', name:'docs: 文档变更' },
{ value: 'style', name:'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'refactor', name:'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name:'perf: 性能优化' },
{ value: 'test', name:'test: 添加、修改测试用例' },
{ value: 'build', name:'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)' },
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name:'chore: 构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name:'revert: 回滚 commit' },
{ value: 'wip', name:'wip: 开发中' },
{ value: 'mod', name:'mod: 不确定分类的修改' },
{ value: 'release', name:'release: 发布' },
{ value: 'feat', name: 'feat: 新增功能' },
{ value: 'fix', name: 'fix: 修复bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 添加、修改测试用例' },
{
value: 'build',
name: 'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)',
},
{ value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name: 'revert: 回滚 commit' },
{ value: 'wip', name: 'wip: 开发中' },
{ value: 'mod', name: 'mod: 不确定分类的修改' },
{ value: 'release', name: 'release: 发布' },
],
scopes: [
['custom', '自定义'],
['projects', '项目搭建'],
['projects', '项目搭建'],
['components', '组件相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
@@ -26,7 +29,7 @@ module.exports = {
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`
name: `${value.padEnd(30)} (${description})`,
}
}),
messages: {
@@ -37,9 +40,9 @@ module.exports = {
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:',
breaking: '列举非兼容性重大的变更(可选):',
footer: '列举出所有变更的 Issues Closed可选。 例如: #31, #34',
confirmCommit: '确认提交?'
confirmCommit: '确认提交?',
},
allowBreakingChanges: ['feat', 'fix'],
subjectLimit: 100,
breaklineChar: '|'
breaklineChar: '|',
}

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
node_modules

12
.env
View File

@@ -1,9 +1,3 @@
VITE_TITLE = '捷兑通 - 平台端'
VITE_PORT = 4000
VITE_WS_URL = 'www.wanzhuanyongcheng.cn/admin/data'
VITE_WS1_URL = 'www.jdt168.com/dice/home'
VITE_GAME_API = 'https://www.jdt168.com'
VITE_TITLE='捷兑通 - 平台端'
VITE_PORT=4000

View File

@@ -1,11 +1,22 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# 是否启用MOCK
VITE_USE_MOCK = false
# 是否启用代理
VITE_USE_PROXY = true
# base api
VITE_BASE_API = '/admin'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH='/'
# 是否启用MOCK
VITE_USE_MOCK=false
# 是否启用代理
VITE_USE_PROXY=true
# base api
VITE_BASE_API='https://test.wanzhuanyongcheng.cn/admin'
VITE_WS1_URL='game.wanzhuanyongcheng.cn/dice/home'
VITE_WS_URL='test.wanzhuanyongcheng.cn/admin/data'
VITE_MER_LOGIN_URL='//localhost:3100/login'
VITE_GAME_API='https://www.jdt168.com'
# 是否启用监控
VITE_SENTRY=false

View File

@@ -1,13 +0,0 @@
# 自定义域名CNAME
# VITE_CNAME = 'template.qszone.com'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/vue-naive-admin/'
VITE_USE_HASH = true
# 是否启用MOCK
VITE_USE_MOCK = true
# base api
VITE_BASE_API = '/api'

View File

@@ -1,16 +1,24 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/static/admin'
VITE_PUBLIC_PATH='/'
# 是否启用MOCK
VITE_USE_MOCK = false
VITE_USE_MOCK=false
# base api
VITE_BASE_API = 'https://www.wanzhuanyongcheng.cn/admin'
# VITE_GAME_API = 'http://www.wanzhuanyongcheng.cn/admin'
VITE_BASE_API='//www.wanzhuanyongcheng.cn/admin'
# 是否启用压缩
VITE_USE_COMPRESS = true
VITE_USE_COMPRESS=true
# 压缩类型
VITE_COMPRESS_TYPE = gzip
VITE_COMPRESS_TYPE=gzip
VITE_WS1_URL='www.jdt168.com/dice/home'
VITE_WS_URL='www.wanzhuanyongcheng.cn/admin/data'
VITE_MER_LOGIN_URL='//jdt-prod-mer.wanzhuanyongcheng.cn/login'
# 是否启用监控
VITE_SENTRY=true

5
.env.sentry-build-plugin Normal file
View File

@@ -0,0 +1,5 @@
# DO NOT commit this file to your repository!
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
# It's used for authentication when uploading source maps.
# You can also set this env variable in your own `.env` files and remove this file.
SENTRY_AUTH_TOKEN="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"

View File

@@ -1,7 +1,28 @@
VITE_PUBLIC_PATH = '/'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH='/'
# 是否启用MOCK
VITE_USE_MOCK = true
VITE_USE_MOCK=false
# 是否启用代理
VITE_USE_PROXY=false
# 是否启用压缩
VITE_USE_COMPRESS=true
# 压缩类型
VITE_COMPRESS_TYPE=gzip
# base api
VITE_BASE_API = '/api'
VITE_BASE_API='//test.wanzhuanyongcheng.cn/admin'
VITE_WS1_URL='game.wanzhuanyongcheng.cn/dice/home'
VITE_WS_URL='test.wanzhuanyongcheng.cn/admin/data'
VITE_GAME_API='https://game.wanzhuanyongcheng.cn'
VITE_MER_LOGIN_URL='//jdt-test-mer.wanzhuanyongcheng.cn/login'
# 是否启用监控
VITE_SENTRY=false

12
.idea/admin.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -36,6 +36,10 @@
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<<<<<<< HEAD
=======
<option name="SMART_TABS" value="true" />
>>>>>>> test
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">

15
.idea/git_toolbox_prj.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

6
.idea/jsLinters/eslint.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

4
.idea/modules.xml generated
View File

@@ -2,7 +2,11 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<<<<<<< HEAD
<module fileurl="file://$PROJECT_DIR$/.idea/jdt-admin.iml" filepath="$PROJECT_DIR$/.idea/jdt-admin.iml" />
=======
<module fileurl="file://$PROJECT_DIR$/.idea/admin.iml" filepath="$PROJECT_DIR$/.idea/admin.iml" />
>>>>>>> test
</modules>
</component>
</project>

6
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

View File

@@ -4,6 +4,6 @@
"antfu.iconify",
"mikestead.dotenv",
"sdras.vue-vscode-snippets",
"cipchk.cssrem",
"cipchk.cssrem"
]
}

View File

@@ -30,5 +30,8 @@
"files.associations": {
"*.env.*": "dotenv",
"*.css": "postcss"
},
"[dockerfile]": {
"editor.defaultFormatter": "ms-azuretools.vscode-docker"
}
}

5
Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
COPY default.conf /etc/nginx/conf.d/default.conf

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Ronnie Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,209 +0,0 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</a>
</p>
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin"><img alt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img alt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p>
<p align='center'>
<b>英文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.md">中文</a>
</p>
### Introduction
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) is a **completely open source free and commercially allowed ** admin templateBased on the latest technology stack of front-end such as `Vue3、Vite3、Pinia、Unocss and Naive UI`. Compared with other more popular backend management templates, this project is more concise, lightweight, fresh style, very low learning costs, ideal for small and medium-sized projects or personal projects.
### Features
- 🍒 Integrated [Naive UI](https://www.naiveui.com)recommended by Evan You.
- 🍑 Integrated login, logout and permission verification.
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments.
- 🍎 Integrated `eslint + prettier`.
- 🍌 Integrated `husky + commitlint`.
- 🍉 Integrated `Mock`.
- 🍍 Integrated `pinia`lightweight, simple and easy to use alternative to vuex.
- 📦 Integrated `unplugin` auto import.
- 🤹 Integrated `iconify` iconsupport custom svg icons.
- 🍇 Integrated `unocss`.
### Preview
[https://template.qszone.com](https://template.qszone.com)
[https://base.isme.top](https://base.isme.top)
### Docs
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
### Getting Started
```shell
# Recommended setup git autocrlf 为 false
git config --global core.autocrlf false
# Clone Project
git clone https://github.com/zclzone/vue-naive-admin.git
cd vue-naive-admin
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # Installed and can be ignored
pnpm i # or npm i
# Start
pnpm dev
```
### Build and Release
```shell
# Test Environment
pnpm build:test
# Github Environment
pnpm build:github
# Prod Environment
pnpm build
```
### Other
```shell
# eslint check
pnpm lint
# eslint check and fix
pnpm lint:fix
# PreviewNeed to build first
pnpm preview
# Commithusky+commitlint
pnpm cz
```
### Directory description
```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS version: Qs Admin
#### source code
- github: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### preview
- [https://admin.qszone.com](https://admin.qszone.com)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### Open source projects that use this project:
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): A full-stack blog project in Golang, the frontend of the blog backend is based on vue-naive-admin and integrates with a real backend service, implementing features such as backend-controlled routing.
### Communication group & About the author
<a href="https://blog.qszone.com/about/">
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
</a>

217
README.md
View File

@@ -1,217 +0,0 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</a>
</p>
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin"><img alt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img alt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p>
<p align='center'>
<b>中文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.EN.md">English</a>
</p>
### 简介
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **完全开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite3、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,学习成本非常低,非常适合中小型项目或者个人项目。
### 功能
- 🍒 集成 [Naive UI](https://www.naiveui.com)
- 🍑 集成登陆、注销及权限验证
- 🍐 集成多环境配置dev、测试、生产和github pages环境
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
- 🍌 集成 `husky + commitlint`,代码提交规范化
- 🍉 集成 `mock` 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍍 集成 `pinia`vuex 的替代方案,轻量、简单、易用
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
- 🍇 集成 `unocss`antfu 开源的原子 css 解决方案,非常轻量
### 预览
[https://template.qszone.com](https://template.qszone.com)
[https://base.isme.top](https://base.isme.top)
### 文档
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
[语雀文档Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
### 快速开始
```shell
# 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf
# 不清楚为什么要这样做的请参考这篇文章https://www.freesion.com/article/4532642129
git config --global core.autocrlf false
# 克隆项目
git clone https://github.com/zclzone/vue-naive-admin.git
# 进入项目目录
cd vue-naive-admin
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # 装了可忽略
pnpm i # 或者 npm i
# 启动
pnpm dev
```
### 构建发布
```shell
# 构建测试环境
pnpm build:test
# 构建github pages环境
pnpm build:github
# 构建生产环境
pnpm build
```
### 其他指令
```shell
# eslint代码格式检查
pnpm lint
# 代码检查并修复
pnpm lint:fix
# 预览发布包效果(需先执行构建指令)
pnpm preview
# 提交代码husky+commitlint
pnpm cz
```
### 目录说明
```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS 版本: Qs Admin
#### 源码
- github: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### 预览
- [https://admin.qszone.com](https://admin.qszone.com)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### 使用该项目的开源项目
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): Golang 全栈博客项目, 博客后台的前端基于 vue-naive-admin对接真实后端服务实现了后端控制路由等特性。
### 入群交流 & 关于作者
<a href="https://blog.qszone.com/about/">
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
</a>
### ☕ 赞助我
> 开源不易,请作者喝杯咖啡吧
<p>
<img src="https://assets.qszone.com/images/zhifu_weixin.jpg" style="width: 220px" />
<img src="https://assets.qszone.com/images/zhifu_zhifubao.jpg" style="width: 220px" />
</p>

View File

@@ -7,7 +7,7 @@ export const PROXY_CONFIG = {
* @转发路径 http://localhost:8080/user
*/
'/admin': {
target: 'https://www.wanzhuanyongcheng.cn',
target: 'https://test.wanzhuanyongcheng.cn',
changeOrigin: true,
// rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},

View File

@@ -14,6 +14,7 @@ import viteCompression from 'vite-plugin-compression'
import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock'
import unplugin from './unplugin'
import { sentryVitePlugin } from '@sentry/vite-plugin'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
@@ -29,12 +30,27 @@ export function createVitePlugins(viteEnv, isBuild) {
if (isBuild) {
plugins.push(
visualizer({
open: true,
open: false,
gzipSize: true,
brotliSize: true,
})
)
}
if (viteEnv.VITE_SENTRY) {
plugins.push(
sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'sentry',
project: 'jdt-admin',
url: 'https://w.huakk.top',
sourcemaps: {
ignore: ['node_modules'],
filesToDeleteAfterUpload: ['dist/**/*.js.map'],
},
})
)
}
return plugins
}

24
default.conf Normal file
View File

@@ -0,0 +1,24 @@
server {
# 监听ipv4
listen 80;
# 监听ipv6
listen [::]:80;
server_name localhost;
location / {
add_header Access-Control-Allow-Headers *;
add_header Access-Control-Allow-Methods *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin *;
add_header Cache-Control no-cache;
add_header Access-Control-Max-Age 1728000;
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@@ -1,3 +0,0 @@
推荐阅读作者在掘金的文章:
[保熟的UnoCSS使用指北优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)

View File

@@ -1,40 +0,0 @@
## 使用 iconify 图标
首先去图标库地址:[icones](https://icones.js.org/) 找合适的图标
### 1. 结合 unocss 使用
```html
<i i-carbon-sun />
<i class="i-carbon-sun" />
```
### 2. 结合插件 unplugin-icons 自定义标签使用
`<icon-[iconify图标名称]`
```html
<icon-ant-design:fullscreen-exit-outlined />
<icon-ant-design:fullscreen-outlined />
```
这种方式还支持自定义 svg 图标,本项目自定义 svg 图标固定放在 src/assets/svg 下
`<icon-custom-[svg图标文件名]`
```
<icon-custom-logo />
```
具体配置参看 build/plugin/unplugin.js
### 3. 结合 Naive UI 的 NIcon 组件封装使用
```html
<!-- iconify图标 -->
<TheIcon icon="material-symbols:delete-outline" />
<!-- 自定义svg图标 -->
<TheIcon icon="logo" type="custom" />
```
封装组件参看 src/components/icon

View File

@@ -1,32 +0,0 @@
## 安装pnpm
### 使用Corepack安装推荐
从 v16.13 开始Node.js 发布了 Corepack 来管理包管理器。 这是一项实验性功能,需要通过运行如下脚本来启用它:
```
npx corepack enable // 可能需要管理员权限
```
这将自动在您的系统上安装 pnpm。 但是,它可能不是最新版本的 pnpm。 若要升级,请检查[最新的 pnpm 版本](https://github.com/pnpm/pnpm/releases/latest) 并运行,如 7.14.0
```
corepack prepare pnpm@7.14.0 --activate
```
如果是 Node.js v16.17 或者更新的版本,可以直接安装最新版本的 pnpm
```
corepack prepare pnpm@latest --activate
```
### 使用npm安装
```
npm i -g pnpm
```
更新,卸了重新装
```
npm uninstall -g pnpm
npm i -g pnpm
```

View File

@@ -3,7 +3,6 @@
"version": "1.0.0",
"scripts": {
"build": "vite build",
"build:github": "vite build --mode github && esno ./build/script",
"build:test": "vite build --mode test",
"cz": "cz",
"dev": "vite",
@@ -11,10 +10,12 @@
"lint:fix": "eslint --fix --ext .js,.vue .",
"lint:staged": "lint-staged",
"prepare": "husky install",
"preview": "vite preview"
"preview": "vite preview",
"lf": "npx prettier --write --end-of-line lf ."
},
"lint-staged": {
"*.{js,vue}": [
"npx prettier --write --end-of-line lf .",
"eslint --ext .js,.vue ."
]
},
@@ -31,29 +32,33 @@
]
},
"dependencies": {
"@unocss/eslint-config": "^0.55.0",
"@vueuse/core": "^10.3.0",
"@sentry/vite-plugin": "^2.10.1",
"@sentry/vue": "^7.81.0",
"@unocss/eslint-config": "^0.55.7",
"@vueuse/core": "^10.6.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"axios": "^1.6.2",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"lodash-es": "^4.17.21",
"md-editor-v3": "^4.2.2",
"md-editor-v3": "^4.8.3",
"mockjs": "^1.1.0",
"pinia": "^2.1.6",
"vite": "^4.4.9",
"pinia": "^2.1.7",
"vite": "^4.5.0",
"vue": "3.3.4",
"vue-router": "^4.2.4",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@iconify/json": "^2.2.100",
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@iconify/json": "^2.2.144",
"@iconify/vue": "^4.1.1",
"@unocss/preset-rem-to-px": "^0.55.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"@unocss/preset-rem-to-px": "^0.55.7",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/compiler-sfc": "^3.3.8",
"@zclzone/eslint-config": "^0.0.4",
"chalk": "^5.3.0",
"commitizen": "^4.3.0",
@@ -63,17 +68,17 @@
"esno": "^0.17.0",
"fs-extra": "^11.1.1",
"husky": "^8.0.3",
"lint-staged": "^13.2.3",
"naive-ui": "^2.34.4",
"lint-staged": "^13.3.0",
"naive-ui": "^2.35.0",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.65.1",
"sass": "^1.69.5",
"unocss": "0.55.0",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"unplugin-auto-import": "^0.16.7",
"unplugin-icons": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-mock": "^2.9.8",
"vite-plugin-svg-icons": "^2.0.1"
}
}

8249
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 825 B

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -8,7 +8,7 @@
},
"naiveThemeOverrides": {
"common": {
"primaryColor": "#409EFFE3",
"primaryColor": "#316C72FF",
"primaryColorHover": "#316C72E3",
"primaryColorPressed": "#2B4C59FF",
"primaryColorSuppl": "#316C72E3",

View File

@@ -3,4 +3,11 @@ import { request } from '@/utils'
export default {
getUser: () => request.get('/user'),
refreshToken: () => request.post('/auth/refreshToken', null, { noNeedTip: true }),
// 上传
uploadImg: (data) =>
request.post('/upload', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
}),
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,51 @@
<template>
<v-chart :loading="loading" class="chart" :option="option" />
</template>
<script setup>
import { use } from 'echarts/core'
import { BarChart, PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
DataZoomComponent,
} from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
use([
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
BarChart,
CanvasRenderer,
PieChart,
DataZoomComponent,
])
import VChart, { THEME_KEY } from 'vue-echarts'
import { provide } from 'vue'
provide(THEME_KEY, 'white')
defineProps({
option: {
type: Object,
required: true,
},
loading: {
type: Boolean,
default: true,
},
})
</script>
<style lang="scss" scoped>
.chart {
width: 100%;
height: 100%;
}
</style>

97
src/components/Editor.vue Normal file
View File

@@ -0,0 +1,97 @@
<template>
<div ref="_ref" style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
/>
<Editor
v-model="htmlValue"
:style="{ height: height + 'px', overflowY: 'hidden' }"
:default-config="editorConfig"
mode="default"
@on-created="handleCreated"
/>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { onBeforeUnmount, shallowRef, ref } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import api from '@/api'
const props = defineProps({
valueHtml: {
type: String,
default: '',
},
height: {
type: Number,
default: 500,
},
})
const emit = defineEmits(['update:valueHtml'])
const editorRef = shallowRef()
const _ref = ref(null)
const toolbarConfig = {}
const editorConfig = { placeholder: '请输入游戏介绍...', MENU_CONF: {} }
const htmlValue = computed({
get() {
return props.valueHtml
},
set(value) {
emit('update:valueHtml', value)
},
})
editorConfig.MENU_CONF['uploadImage'] = {
allowedFileTypes: ['image/*'],
base64LimitSize: 5 * 1024, // 5kb
// 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒
async customUpload(file, insertFn) {
await upFile(file, insertFn)
},
}
editorConfig.MENU_CONF['uploadVideo'] = {
allowedFileTypes: ['video/*'],
async customUpload(file, insertFn) {
await upFile(file, insertFn)
},
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // 记录 editor 实例,重要!
editor.on('fullScreen', () => {
_ref.value.style.zIndex = 10
})
}
const upFile = async (file, insertFn) => {
const formData = new FormData()
formData.append('file', file)
const res = await api.uploadImg(formData)
insertFn(res.data.data)
}
</script>
<style lang="scss" scoped></style>

View File

@@ -1,7 +1,9 @@
<template>
<router-view v-slot="{ Component, route }">
<KeepAlive :include="keepAliveNames">
<component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
<div h-full w-full>
<component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
</div>
</KeepAlive>
</router-view>
</template>

View File

@@ -89,11 +89,11 @@ function getMenuItem(route, basePath = '') {
function getIcon(meta) {
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
if (meta?.icon !== '无' && meta?.icon) return renderIcon(meta.icon, { size: 18 })
return null
}
function handleMenuSelect(key, item) {
function handleMenuSelect(_, item) {
if (isExternal(item.path)) {
window.open(item.path)
} else {

View File

@@ -18,6 +18,27 @@ async function setupApp() {
await setupRouter(app)
app.directive('perms', {
mounted: (el, binding) => {
const { value } = binding
const permissions = JSON.parse(localStorage.getItem('roles'))
const all_permission = '*'
if (Array.isArray(value)) {
if (value.length > 0) {
const hasPermission = permissions.some((key) => {
return all_permission === key || value.includes(key)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
} else {
throw new Error('like v-perms="[\'auth/menu/edit\']"')
}
},
})
app.mount('#app')
}

View File

@@ -2,9 +2,10 @@ import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router
import { setupRouterGuard } from './guard'
import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
import { getToken, isNullOrWhitespace } from '@/utils'
import { useUserStore, usePermissionStore } from '@/store'
import { usePermissionStore } from '@/store'
import * as Sentry from '@sentry/vue'
const isHash = true
const isHash = false
export const router = createRouter({
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
routes: basicRoutes,
@@ -14,19 +15,25 @@ export const router = createRouter({
export async function setupRouter(app) {
await addDynamicRoutes()
setupRouterGuard(router)
if (import.meta.env.VITE_SENTRY === 'true') {
Sentry.init({
app,
dsn: 'https://1c158d5f832eef396e69447959d902d2@w.huakk.top/12',
integrations: [
new Sentry.BrowserTracing({
tracePropagationTargets: ['localhost', /^https:\/\/w\.huakk\.top\/api/],
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
}),
new Sentry.Replay(),
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}
app.use(router)
}
export async function resetRouter() {
const basicRouteNames = getRouteNames(basicRoutes)
router.getRoutes().forEach((route) => {
const name = route.name
if (!basicRouteNames.includes(name)) {
router.removeRoute(name)
}
})
}
export async function addDynamicRoutes() {
const token = getToken()
@@ -38,10 +45,8 @@ export async function addDynamicRoutes() {
// 有token的情况
try {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
!userStore.userId && (await userStore.getUserInfo())
const accessRoutes = permissionStore.generateRoutes(userStore.role)
const accessRoutes = permissionStore.generateRoutes()
accessRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
@@ -49,6 +54,7 @@ export async function addDynamicRoutes() {
router.addRoute(NOT_FOUND_ROUTE)
} catch (error) {
console.error(error)
throw error
}
}
@@ -58,8 +64,8 @@ export function getRouteNames(routes) {
function getRouteName(route) {
const names = [route.name]
if (route.children && route.children.length) {
names.push(...route.children.map((item) => getRouteName(item)).flat(1))
if (route.subMenu && route.subMenu.length) {
names.push(...route.subMenu.map((item) => getRouteName(item)).flat(1))
}
return names
}

View File

@@ -1,5 +1,3 @@
// const Layout = () => import('@/layout/index.vue')
export const basicRoutes = [
{
name: '404',
@@ -17,43 +15,6 @@ export const basicRoutes = [
title: '登录页',
},
},
// {
// name: 'ExternalLink',
// path: '/external-link',
// component: Layout,
// meta: {
// title: '外部链接',
// icon: 'mdi:link-variant',
// order: 4,
// },
// children: [
// {
// name: 'LinkGithubSrc',
// path: 'https://github.com/zclzone/vue-naive-admin',
// meta: {
// title: '源码 - github',
// icon: 'mdi:github',
// },
// },
// {
// name: 'LinkGiteeSrc',
// path: 'https://gitee.com/zclzone/vue-naive-admin',
// meta: {
// title: '源码 - gitee',
// icon: 'simple-icons:gitee',
// },
// },
// {
// name: 'LinkDocs',
// path: 'https://zclzone.github.io/vue-naive-admin-docs',
// meta: {
// title: '文档 - vuepress',
// icon: 'mdi:vuejs',
// },
// },
// ],
// },
]
export const NOT_FOUND_ROUTE = {
@@ -68,11 +29,3 @@ export const EMPTY_ROUTE = {
path: '/:pathMatch(.*)*',
component: null,
}
const modules = import.meta.glob('@/views/**/route.js', { eager: true })
const asyncRoutes = []
Object.keys(modules).forEach((key) => {
asyncRoutes.push(modules[key].default)
})
export { asyncRoutes }

View File

@@ -1,33 +1,104 @@
import { defineStore } from 'pinia'
import { asyncRoutes, basicRoutes } from '@/router/routes'
import { basicRoutes } from '@/router/routes'
import { RouterView } from 'vue-router'
function hasPermission(route, role) {
// * 不需要权限直接返回true
if (!route.meta?.requireAuth) return true
const Layout = () => import('@/layout/index.vue')
const routeRole = route.meta?.role ? route.meta.role : []
// 匹配views里面所有的.vue文件动态引入
const modules = import.meta.glob('/src/views/**/*.vue')
// * 登录用户没有角色或者路由没有设置角色判定为没有权限
if (!role.length || !routeRole.length) return false
// * 路由指定的角色包含任一登录用户角色则判定有权限
return role.some((item) => routeRole.includes(item))
//
export function getModulesKey() {
return Object.keys(modules).map((item) => item.replace('/src/views/', '').replace('.vue', ''))
}
function filterAsyncRoutes(routes = [], role) {
// 动态加载组件
export function loadRouteView(component) {
try {
const key = Object.keys(modules).find((key) => {
return key.includes(`${component}.vue`)
})
if (key) {
return modules[key]
}
throw Error(`找不到组件${component},请确保组件路径正确`)
} catch (error) {
console.error(error)
return RouterView
}
}
// function hasPermission(route, role) {
// // * 不需要权限直接返回true
// if (!route.meta?.requireAuth) return true
// const routeRole = route.meta?.role ? route.meta.role : []
// // * 登录用户没有角色或者路由没有设置角色判定为没有权限
// if (!role.length || !routeRole.length) return false
// // * 路由指定的角色包含任一登录用户角色则判定有权限
// return role.some((item) => routeRole.includes(item))
// }
// 过滤异步路由
function filterAsyncRoutes(routes = [], firstRoute = true) {
const ret = []
routes.forEach((route) => {
if (hasPermission(route, role)) {
const curRoute = {
...route,
children: [],
}
if (route.children && route.children.length) {
curRoute.children = filterAsyncRoutes(route.children, role)
} else {
Reflect.deleteProperty(curRoute, 'children')
}
ret.push(curRoute)
// 过滤掉type为3的路由
if (route.type === 3) return
const isHidden = route.is_show === 1 ? false : true
const meta = {
requireAuth: true,
title: route.name,
icon: route.icon,
order: route.sort,
}
const curRoute = {
path: route.route,
name: route.name,
isHidden,
meta,
children: [],
}
if (route.route === '/' && firstRoute) {
curRoute['redirect'] = route.subMenu[0].route
} else if (route.subMenu && route.type === 1) {
curRoute['redirect'] = `${route.subMenu[0].route}`
}
if (route.subMenu && route.subMenu.length) {
curRoute.children = filterAsyncRoutes(route.subMenu, false)
} else {
Reflect.deleteProperty(curRoute, 'children')
}
switch (route.type) {
case 1:
curRoute.component = firstRoute ? Layout : RouterView
break
case 2:
curRoute.component = loadRouteView(route.components)
break
}
ret.push(curRoute)
})
return ret
}
// 递归寻找type为3的路由
function findType3Routes(routes = []) {
const ret = []
routes.forEach((route) => {
if (route.type === 3) {
ret.push(route.api_route)
}
if (route.subMenu && route.subMenu.length) {
ret.push(...findType3Routes(route.subMenu))
}
})
return ret
@@ -48,8 +119,10 @@ export const usePermissionStore = defineStore('permission', {
},
},
actions: {
generateRoutes(role = []) {
const accessRoutes = filterAsyncRoutes(asyncRoutes, role)
generateRoutes() {
const menus = JSON.parse(localStorage.getItem('menu'))
const accessRoutes = filterAsyncRoutes(menus)
window.localStorage.setItem('roles', JSON.stringify(findType3Routes(menus)))
this.accessRoutes = accessRoutes
return accessRoutes
},

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { resetRouter } from '@/router'
// import { resetRouter } from '@/router'
import { useTagsStore, usePermissionStore } from '@/store'
import { removeToken, toLogin } from '@/utils'
// import api from '@/api'
@@ -18,7 +18,10 @@ export const useUserStore = defineStore('user', {
return this.userInfo?.name
},
avatar() {
return this.userInfo?.avatar
return (
this.userInfo?.avatar ||
'https://pic3.58cdn.com.cn/nowater/webim/big/n_v21bc7874294754e63a22b80febac9cf51.jpg'
)
},
role() {
return this.userInfo?.role || []
@@ -41,7 +44,7 @@ export const useUserStore = defineStore('user', {
removeToken()
resetTags()
resetPermission()
resetRouter()
// resetRouter()
this.$reset()
toLogin()
},

View File

@@ -24,6 +24,9 @@ export function resolveResError(code, message) {
case 500:
message = message ?? '服务器异常'
break
case 402:
message = message ?? '无权限访问'
break
default:
message = message ?? `${code}】: 未知异常!`
break

View File

@@ -14,4 +14,6 @@ export function createAxios(options = {}) {
return service
}
export const request = createAxios({})
export const request = createAxios({
baseURL: import.meta.env.VITE_BASE_API,
})

View File

@@ -2,11 +2,11 @@ import { getToken } from '@/utils'
import { resolveResError } from './helpers'
export function reqResolve(config) {
if (config.url.includes('/dice')) {
config.baseURL = import.meta.env.VITE_GAME_API
} else {
config.baseURL = import.meta.env.VITE_BASE_API
}
// if (config.url.includes('/admin')) {
// config.baseURL = import.meta.env.VITE_ADMIN_API
// } else {
// config.baseURL = import.meta.env.VITE_BASE_API
// }
// 处理不需要token的请求
if (config.noNeedToken) {
return config
@@ -37,7 +37,7 @@ export function resResolve(response) {
const code = data?.code ?? status
/** 根据code处理对应的操作并返回处理后的message */
const message = resolveResError(code, data?.message ?? statusText)
const message = resolveResError(code, data?.msg ?? statusText)
/** 需要错误提醒 */
!config.noNeedTip && window.$message?.error(message)

View File

@@ -0,0 +1,6 @@
import { request } from '@/utils'
export default {
getList: (data) => request.post('/store/classify', data),
addClass: (data) => request.post('/store/classify/edit', data),
}

View File

@@ -1,6 +1,8 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-button type="primary" @click="handleAdd(1)">新增商品分类</n-button>
<n-button v-perms="['/admin/store/classify/edit']" type="primary" @click="handleAdd(1)">
新增商户分类
</n-button>
<!-- {{ formValue }} -->
<n-data-table
:loading="loading"
@@ -8,6 +10,9 @@
:data="data"
:pagination="pagination"
:bordered="false"
remote
:row-key="rowKey"
children-key="Classify"
/>
<n-modal v-model:show="showModal">
<n-card
@@ -45,12 +50,17 @@
</template>
<script setup>
import { onMounted, h } from 'vue'
import { onMounted, h, withDirectives, resolveDirective } from 'vue'
import api from './api'
import { NButton } from 'naive-ui'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const rowKey = (row) => {
return row.Classify || []
}
const columns = ref([
{
title: 'ID',
@@ -76,17 +86,20 @@ const columns = ref([
slot: 'action',
render(row) {
return [
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
formValue.value = row
handleAdd(2)
withDirectives(
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
formValue.value = row
handleAdd(2)
},
},
},
() => '编辑'
() => '编辑'
),
[[vPerms, ['/admin/store/classify/edit']]]
),
]
},
@@ -105,6 +118,7 @@ const rules = {
}
const formValue = ref({
ID: 0,
name: '',
status: 1,
})
@@ -112,9 +126,9 @@ const formValue = ref({
const showModal = ref(false)
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
total: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -133,12 +147,12 @@ onMounted(() => {
const getList = async () => {
loading.value = true
try {
const res = await api.getMerClass({
pageNum: pagination.value.current,
const res = await api.getList({
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
console.log(res)
data.value = res.data.data
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
}
@@ -148,12 +162,13 @@ const getList = async () => {
const modelTitle = ref('')
const handleAdd = (e) => {
modelTitle.value = e === 1 ? '新增商分类' : '编辑商分类'
modelTitle.value = e === 1 ? '新增商分类' : '编辑商分类'
showModal.value = true
}
const clear = () => {
formValue.value = {
ID: 0,
name: '',
status: 1,
}
@@ -165,7 +180,7 @@ const handleValidateClick = async (e) => {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
await api.addMerClass(formValue.value)
await api.addClass(formValue.value)
$message.success('成功')
clear()
getList()

View File

@@ -4,4 +4,6 @@ export default {
getList: (data) => request.post('/store', data),
addMer: (data) => request.post('/store/edit', data),
getMerType: () => request.post('/store/getOther'),
// 一键登录
login: (data) => request.post('/store/easy/login', data),
}

View File

@@ -1,7 +1,7 @@
<template>
<CommonPage show-footer :title="$route.title">
<!-- {{ formValue }} -->
<n-button type="primary" @click="handleAdd(1)">新增商户</n-button>
<n-button v-perms="['/store/edit']" type="primary" @click="handleAdd(1)">新增商户</n-button>
<n-grid class="mb-10" x-gap="12" cols="6" collapsed>
<n-gi>
<div class="flex items-center">
@@ -41,6 +41,7 @@
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<n-drawer v-model:show="showModal" :width="502" placement="right">
@@ -54,23 +55,26 @@
:rules="rules"
size="medium"
>
<!-- <n-form-item label="商户头像:" path="img">
<n-upload
v-model:file-list="formValue.img"
action="https://www.mocky.io/v2/5e4bafc63100007100d8b70f"
list-type="image-card"
>
点击上传
</n-upload>
</n-form-item> -->
<n-form-item label="商户名称:" path="name">
<n-input v-model:value="formValue.name" placeholder="请输入商户名称" />
<n-input
v-model:value="formValue.name"
:disabled="isEdit"
placeholder="请输入商户名称"
/>
</n-form-item>
<n-form-item label="负责人姓名:" path="username">
<n-input v-model:value="formValue.username" placeholder="请输入负责人姓名" />
<n-input
v-model:value="formValue.username"
:disabled="isEdit"
placeholder="请输入负责人姓名"
/>
</n-form-item>
<n-form-item label="商户手机号:" path="phone">
<n-input v-model:value="formValue.phone" placeholder="请输入商户手机号" />
<n-input
v-model:value="formValue.phone"
:disabled="isEdit"
placeholder="请输入商户手机号"
/>
</n-form-item>
<n-form-item label="商户座机:" path="mobile">
<n-input v-model:value="formValue.mobile" placeholder="请输入商户座机" />
@@ -78,9 +82,9 @@
<n-form-item label="商户地址:" path="address">
<n-input v-model:value="formValue.address" placeholder="请输入商户地址" />
</n-form-item>
<n-form-item label="经营类目:" path="classId">
<n-form-item label="经营类目:" path="store_class_id">
<n-select
v-model:value="formValue.classId"
v-model:value="formValue.store_class_id"
label-field="name"
value-field="ID"
clearable
@@ -88,23 +92,22 @@
:options="classOptions"
/>
</n-form-item>
<!-- <n-form-item label="商户经纬度:" path="local">
<n-input v-model:value="formValue.local" placeholder="请输入商户地址" />
<div ref="wrapRef" class="h-300 w-300"></div>
</n-form-item> -->
<n-form-item label="商户密码:" path="password">
<n-form-item v-if="!isEdit" label="商户密码:" path="password">
<n-input v-model:value="formValue.password" placeholder="请输入商户密码" />
</n-form-item>
<n-form-item label="商户类型:" path="bType">
<n-select
v-model:value="formValue.bType"
label-field="name"
value-field="ID"
placeholder="请选择商户类型"
clearable
:options="typeOptions"
/>
<n-form-item v-else label="修改密码:" path="password">
<n-input v-model:value="formValue.password" placeholder="不修改密码请留空" />
</n-form-item>
<!-- <n-form-item label="商户类型:" path="bType">-->
<!-- <n-select-->
<!-- v-model:value="formValue.bType"-->
<!-- label-field="name"-->
<!-- value-field="ID"-->
<!-- placeholder="请选择商户类型"-->
<!-- clearable-->
<!-- :options="typeOptions"-->
<!-- />-->
<!-- </n-form-item>-->
<n-form-item label="手续费收取类型:" path="scaleType">
<n-select
v-model:value="formValue.scaleType"
@@ -137,7 +140,7 @@
>
提交
</n-button>
<n-button class="m-auto w-200" @click="handleClearValidateClick">重置</n-button>
<!-- <n-button class="m-auto w-200" @click="handleClearValidateClick">重置</n-button> -->
</n-form-item>
</n-form>
</n-drawer-content>
@@ -146,10 +149,13 @@
</template>
<script setup>
import { onMounted, ref, h, unref, nextTick } from 'vue'
import { onMounted, ref, h, withDirectives, resolveDirective } from 'vue'
import { NButton } from 'naive-ui'
import api from './api'
import { useScript } from '@/hooks/useScript'
const vPerms = resolveDirective('perms')
const isEdit = computed(() => drawerTitle.value === '编辑商户')
const columns = ref([
{
@@ -157,18 +163,10 @@ const columns = ref([
align: 'center',
key: 'name',
},
{
title: '商户类型',
align: 'center',
key: 'type',
render(row) {
return h('span', row.bType === 1 ? '供应商' : '兑换商')
},
},
{
title: '状态',
align: 'center',
key: 'status',
slot: 'status',
render(row) {
return h('span', row.status === 1 ? '正常' : '禁用')
},
@@ -177,19 +175,47 @@ const columns = ref([
title: '操作',
align: 'center',
slot: 'action',
render(row) {
render: (row) => {
return [
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
formValue.value = row
handleAdd(2)
withDirectives(
h(
NButton,
{
type: 'primary',
text: true,
size: 'small',
onClick: () => {
formValue.value = { ...row }
Reflect.deleteProperty(formValue.value, 'password')
handleAdd(2)
},
},
},
() => '编辑'
() => '编辑'
),
[[vPerms, ['/admin/store/edit']]]
),
withDirectives(
h(
NButton,
{
class: 'ml-10',
type: 'primary',
text: true,
size: 'small',
onClick: async () => {
const res = await api.login({
bid: row.bid,
})
window.open(
`${import.meta.env.VITE_MER_LOGIN_URL}?redirect=/workbench&type=${
res.data.type
}&tk=${res.data.token}`
)
},
},
() => '一键登录'
),
[[vPerms, ['/admin/store/login']]]
),
]
},
@@ -204,12 +230,12 @@ const showModal = ref(false)
const formRef = ref(null)
const drawerTitle = ref('')
const drawerTitle = ref('新增商户')
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
total: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -226,31 +252,15 @@ const QuryVal = ref({
Status: null,
})
// const defaultValueRef = () => ({
// name: '',
// username: '',
// phone: '',
// mobile: '',
// address: '',
// classId: null,
// local: '',
// password: '',
// bType: null,
// scaleType: null,
// scale: null,
// status: 2,
// })
let formValue = ref({
name: '',
username: '',
phone: '',
mobile: '',
address: '',
classId: null,
store_class_id: null,
local: '',
password: '',
bType: null,
scaleType: null,
scale: null,
status: 2,
@@ -287,7 +297,7 @@ const rules = {
message: '请搜索商户经纬度',
trigger: 'blur',
},
classId: {
store_class_id: {
required: true,
type: 'number',
message: '请选择经营类目',
@@ -298,12 +308,6 @@ const rules = {
// message: '请输入商户密码',
// trigger: 'blur',
// },
bType: {
required: true,
type: 'number',
message: '请选择商户类型',
trigger: 'change',
},
scaleType: {
required: true,
type: 'number',
@@ -323,42 +327,20 @@ const rules = {
},
}
const wrapRef = ref(null)
const MapUrl =
'https://map.qq.com/api/gljs?v=1.exp&key=S3GBZ-WR26O-IXNW2-SXBOD-LZXV6-WAFNO&callback=initMap'
const { toPromise } = useScript({ src: MapUrl })
onMounted(() => {
initMap()
getList()
getMertype()
})
const initMap = async () => {
await toPromise()
await nextTick()
const wrapEl = unref(wrapRef.value)
if (!wrapEl) return
const TMap = window?.TMap
const center = new TMap.Map.LatLng(39.984104, 116.307503)
const map = new TMap.Map.Map(wrapEl, {
rotation: 20, //设置地图旋转角度
pitch: 30, //设置俯仰角度0~45
zoom: 12, //设置地图缩放级别
center: center, //设置地图中心点坐标
})
console.log(map)
}
const getList = async () => {
loading.value = true
const res = await api.getList({
...QuryVal.value,
PageNum: pagination.value.current,
PageNum: pagination.value.page,
PageSize: pagination.value.pageSize,
})
data.value = res.data.data
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
loading.value = false
}
@@ -374,7 +356,7 @@ const getMertype = async () => {
const clearQuryVal = () => {
QuryVal.value = {
StoreName: '',
Status: '',
Status: null,
}
getList()
}
@@ -384,14 +366,6 @@ const handleAdd = (e) => {
showModal.value = true
}
// const onPositiveClick = () => {
// showModal.value = false
// }
// const onNegativeClick = () => {
// showModal.value = false
// }
const handleValidateClick = (e) => {
e.preventDefault()
formRef.value?.validate(async (errors) => {
@@ -400,8 +374,8 @@ const handleValidateClick = (e) => {
await api.addMer(formValue.value)
$message.success('成功')
handleClearValidateClick()
getMertype()
getList()
await getMertype()
await getList()
showModal.value = false
} catch (error) {
$message.error(error.msg)

View File

@@ -1,6 +1,8 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-button type="primary" @click="handleAdd(1)">新增商户类型</n-button>
<n-button v-perms="['/admin/typesof/edit']" type="primary" @click="handleAdd(1)">
新增商户类型
</n-button>
<!-- {{ formValue }} -->
<n-data-table
:loading="loading"
@@ -8,6 +10,7 @@
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<n-modal v-model:show="showModal">
<n-card
@@ -45,9 +48,10 @@
</template>
<script setup>
import { onMounted, h } from 'vue'
import { onMounted, h, withDirectives, resolveDirective } from 'vue'
import api from './api'
import { NButton } from 'naive-ui'
const vPerms = resolveDirective('perms')
const loading = ref(false)
@@ -76,17 +80,20 @@ const columns = ref([
slot: 'action',
render(row) {
return [
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
formValue.value = row
handleAdd(2)
withDirectives(
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
formValue.value = row
handleAdd(2)
},
},
},
() => '编辑'
() => '编辑'
),
[[vPerms, ['/admin/typesof/edit']]]
),
]
},
@@ -112,9 +119,9 @@ const formValue = ref({
const showModal = ref(false)
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
total: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -134,11 +141,11 @@ const getList = async () => {
loading.value = true
try {
const res = await api.getMerType({
pageNum: pagination.value.current,
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
console.log(res)
data.value = res.data.data
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
}

View File

@@ -0,0 +1,8 @@
import { request } from '@/utils'
export default {
// 获取入驻审核列表
getAuditList: (data) => request.post('/process/store', data),
// 通过审核/不通过
passAudit: (data) => request.post('/process/store/edit', data),
}

View File

@@ -1,7 +1,211 @@
<template>
<CommonPage show-footer :title="$route.title"></CommonPage>
<CommonPage show-footer :title="$route.title">
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<!-- 详情 -->
<n-drawer v-model:show="active" :width="502" placement="right">
<n-drawer-content title="商户入驻详情">
<div>
<div>商户名称:{{ nowRow.name }}</div>
<div mt-10>用户姓名:{{ nowRow.username }}</div>
<div mt-10>联系电话:{{ nowRow.phone }}</div>
<div mt-10>开户行:{{ nowRow.bank }}</div>
<div mt-10>银行卡号:{{ nowRow.bank_card }}</div>
<!-- <div mt-10>商户类型:{{ atype.name }}</div>-->
<div mt-10>经营类目:{{ btype.name }}</div>
<div mt-10>
<div>营业执照:</div>
<n-image width="100" :src="nowRow.license" />
</div>
<div mt-10>
<div>法人身份证正面:</div>
<n-image width="100" :src="nowRow.front" />
</div>
<div mt-10>
<div>法人身份证反面:</div>
<n-image width="100" :src="nowRow.back" />
</div>
<div mt-10>
<div>门头照:</div>
<n-image-group>
<n-image
v-for="(item, index) in nowRow.img"
:key="index"
mr-10
width="100"
:src="item"
/>
</n-image-group>
</div>
</div>
<div m-auto w-full flex justify-center>
<n-button mr-20 type="primary" @click="ok">通过</n-button>
<n-button mr-20 type="warning" @click="noOk">不通过</n-button>
<n-button @click="active = false">关闭</n-button>
</div>
</n-drawer-content>
</n-drawer>
</CommonPage>
</template>
<script setup></script>
<script setup>
import { h, withDirectives, resolveDirective } from 'vue'
import api from './api'
import api1 from '../mer_list/api'
import { NButton } from 'naive-ui'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const nowRow = ref({})
const active = ref(false)
const columns = ref([
{
title: '商户名称',
align: 'center',
key: 'name',
},
{
title: '用户姓名',
align: 'center',
key: 'username',
},
{
title: '联系电话',
align: 'center',
key: 'phone',
},
{
title: '开户银行',
align: 'center',
key: 'bank',
},
{
title: '银行卡号',
align: 'center',
key: 'bank_card',
},
{
title: '操作',
align: 'center',
slot: 'detail',
render: (row) => {
return [
withDirectives(
h(
NButton,
{
type: 'primary',
text: true,
onClick: () => {
nowRow.value = {
...row,
img: row.img.split(','),
}
console.log(nowRow.value)
active.value = true
},
},
{
default: () => '详情',
}
),
[[vPerms, ['/admin/process/store/edit']]]
),
]
},
},
])
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
},
})
onMounted(() => {
getData()
getMertype()
})
const getData = async () => {
loading.value = true
const res = await api.getAuditList({
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
loading.value = false
}
const classOptions = ref([])
// const typeOptions = ref([])
const getMertype = async () => {
const res = await api1.getMerType()
classOptions.value = res.data.class
// typeOptions.value = res.data.type
}
// const atype = computed(() => {
// return typeOptions.value.find((item) => {
// if (item.ID === nowRow.value.bType) return item
// })
// })
const btype = computed(() => {
return classOptions.value.find((item) => {
if (item.ID === nowRow.value.store_class_id) return item
})
})
const ok = async () => {
$dialog.warning({
title: '提示',
content: '同意后无法撤销,确认同意吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
const res = await api.passAudit({
bid: nowRow.value.bid,
status: 1,
})
$message.success(res.msg)
clear()
},
onNegativeClick: () => {
$message.warning('已取消操作')
},
})
}
const noOk = async () => {
const res = await api.passAudit({
bid: nowRow.value.bid,
status: 2,
})
$message.success(res.msg)
clear()
}
const clear = () => {
nowRow.value = {}
active.value = false
getData()
}
</script>
<style lang="scss" scoped></style>

View File

@@ -5,6 +5,11 @@ export default {
path: '/merchant',
component: Layout,
redirect: '/mer_list',
meta: {
title: '商户管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'Merlist',
@@ -16,6 +21,16 @@ export default {
order: 10,
},
},
{
name: 'Classlist',
path: 'mer_class',
component: () => import('./mer_class/index.vue'),
meta: {
title: '商户分类',
icon: 'mdi:account-multiple',
order: 10,
},
},
{
name: 'Mertype',
path: 'mer_type',

View File

@@ -1,6 +0,0 @@
import { request } from '@/utils'
export default {
getMerClass: (data) => request.post('/classify', data),
addMerClass: (data) => request.post('/classify/edit', data),
}

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<CommonPage show-footer :title="$route.title">
<n-data-table
@@ -8,43 +9,248 @@
:bordered="false"
remote
/>
<!-- 拒绝 -->
<n-modal v-model:show="isNoteModel">
<n-card
style="width: 500px"
title="拒绝信息"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-input v-model:value="notesVal" type="textarea" placeholder="请输入拒绝理由...." />
<div m-auto p-10>
<n-button type="primary" @click="veeify">确定</n-button>
<n-button ml-10 @click="clear">取消</n-button>
</div>
</n-card>
</n-modal>
<!-- 豆子设置 -->
<n-modal v-model:show="isDzModel">
<n-card
style="width: 500px"
title="赠送/比例"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formRef" :model="nowRow" :rules="rules" label-placement="left">
<n-grid :cols="24">
<n-form-item-gi :span="20" label="商品赠送豆子" path="pulse">
<n-input-number
v-model:value="nowRow.pulse"
clearable
placeholder="请输入赠送豆子数量...."
:min="0"
/>
</n-form-item-gi>
<n-form-item-gi :span="20" label="商品赠送积分" path="integral">
<n-input-number
v-model:value="nowRow.integral"
clearable
placeholder="请输入赠送积分数量...."
:min="0"
/>
</n-form-item-gi>
<n-form-item-gi :span="18" label="商品分佣类型" path="commission_type">
<n-select
v-model:value="nowRow.commission_type"
placeholder="请选择分佣类型"
clearable
:options="[
{
label: '百分比',
value: 1,
},
{
label: '数值',
value: 2,
},
]"
/>
</n-form-item-gi>
<n-form-item-gi :span="20" label="商品分佣比例" path="commission">
<n-input-number
v-model:value="nowRow.commission"
clearable
placeholder="请输入分佣比例...."
:min="0"
/>
</n-form-item-gi>
<n-form-item-gi :span="20" label="豆子过期时间" path="expiration">
<n-input-number
v-model:value="nowRow.expiration"
clearable
placeholder="请输入豆子过期时间"
:min="0"
/>
</n-form-item-gi>
<n-form-item-gi :span="12">
<div m-auto p-10>
<n-button type="primary" @click="veeify">确定</n-button>
<n-button ml-10 @click="clear">取消</n-button>
</div>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
</n-modal>
<!-- 商品详情 -->
<n-drawer v-model:show="showDrawer" :width="502">
<n-drawer-content title="商品详情" closable>
<n-space vertical>
<div>
<span>商品分类</span>
<span>{{ goodInfo.class_name }}</span>
</div>
<div>
<span>商品名称</span>
<span>{{ goodInfo.name }}</span>
</div>
<div flex items-center>
<span>封面</span>
<n-image width="100" :src="goodInfo.cover" />
</div>
<div flex items-center>
<span>轮播图</span>
<div w-400 overflow-auto>
<n-image
v-for="(url, index) in goodInfo.rotation?.split(',')"
:key="index"
width="100"
:src="url"
/>
</div>
</div>
<div>
<span>商品价格</span>
<span>{{ goodInfo.number }}</span>
</div>
<div>
<span>市场价格</span>
<span>{{ goodInfo.market_num }}</span>
</div>
<div>
<span>商品库存</span>
<span>{{ goodInfo.stock }}</span>
</div>
<div>
<span>商品简介</span>
<span>{{ goodInfo.profile }}</span>
</div>
<div>
<span>商品详情</span>
<div v-html="goodInfo.details"></div>
</div>
</n-space>
</n-drawer-content>
</n-drawer>
</CommonPage>
</template>
<script setup>
import api from './api'
import { NDropdown, NButton } from 'naive-ui'
import { h } from 'vue'
import { NButton, NImage, NSpace, NEllipsis } from 'naive-ui'
import { h, withDirectives, resolveDirective } from 'vue'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const isNoteModel = ref(false)
const isDzModel = ref(false)
const goodInfo = ref({})
const showDrawer = ref(false)
const notesVal = ref('')
const formRef = ref(null)
const rules = {
pulse: {
required: true,
type: 'number',
message: '请输入赠送豆子数量',
trigger: 'blur',
},
integral: {
required: true,
type: 'number',
message: '请输入赠送积分数量',
trigger: 'blur',
},
commission_type: {
required: true,
type: 'number',
message: '请选择分佣类型',
trigger: 'change',
},
commission: {
required: true,
type: 'number',
message: '请输入分佣比例',
trigger: 'blur',
},
expiration: {
required: true,
type: 'number',
message: '请输入豆子过期时间',
trigger: 'blur',
},
}
const nowRow = ref({})
const nowKey = ref(null)
const columns = ref([
{
title: '商品名称',
key: 'name',
slot: 'name',
align: 'center',
render: (row) => {
return h(
NEllipsis,
{
style: 'max-width: 200px',
},
{
default: () => row.name,
}
)
},
},
{
title: '商品封面',
slot: 'cover',
align: 'center',
render(row) {
return h('img', {
return h(NImage, {
src: row.cover,
style: {
width: '30px',
height: '30px',
},
width: '30',
})
},
},
{
title: '商品分类',
key: 'class_name',
slot: 'Classify',
align: 'center',
render(row) {
return h(
'div',
{},
{
default: () => row.Classify.name,
}
)
},
},
{
title: '商品价格',
title: '商品价格(元)',
key: 'number',
align: 'center',
},
@@ -53,52 +259,130 @@ const columns = ref([
key: 'stock',
align: 'center',
},
{
title: '赠送豆子',
key: 'pulse',
align: 'center',
},
{
title: '赠送积分',
key: 'integral',
align: 'center',
},
{
title: '分佣类型',
slot: 'commission_type',
align: 'center',
render(row) {
return row.commission_type === 1 ? '百分比' : '数值'
},
},
{
title: '分佣比例',
key: 'commission',
align: 'center',
},
{
title: '商品状态',
slot: 'status',
align: 'center',
render(row) {
return row.status === 0 ? '待审核' : row.status === 1 ? '已审核' : '已拒绝'
return row.status === 3 ? '待审核' : row.status === 1 ? '已审核' : '已拒绝'
},
},
{
title: '备注',
key: 'notes',
align: 'center',
},
{
title: '操作',
slot: 'action',
align: 'center',
render(row) {
const el = []
if (row.status === 0) {
el.push(
let el = []
if (row.status === 3) {
el = [
h(
NDropdown,
NSpace,
{
trigger: 'click',
options: [
{
label: '审核',
key: 1,
},
{
label: '拒绝',
key: 2,
},
],
onSelect: (key) => {
veeify(key, row)
},
justify: 'center',
},
() =>
h(
NButton,
{
type: 'primary',
text: true,
{
default: () => [
withDirectives(
h(
NButton,
{
type: 'primary',
text: true,
onClick: () => {
nowKey.value = 1
nowRow.value = { ...row }
veeify()
},
},
() => '审核通过'
),
[[vPerms, ['/admin/goods/process']]]
),
withDirectives(
h(
NButton,
{
type: 'error',
text: true,
onClick: () => {
nowKey.value = 2
nowRow.value = { ...row }
isNoteModel.value = true
},
},
() => '审核拒绝'
),
[[vPerms, ['/admin/goods/process']]]
),
withDirectives(
h(
NButton,
{
type: 'warning',
text: true,
onClick: () => {
nowKey.value = 3
goodInfo.value = { ...row }
showDrawer.value = true
},
},
() => '商品详情'
),
[[vPerms, ['/admin/goods/process']]]
),
],
}
),
]
} else {
el = [
withDirectives(
h(
NButton,
{
type: 'info',
text: true,
onClick: () => {
nowRow.value = { ...row }
nowKey.value = 3
isDzModel.value = true
},
() => '审核'
)
)
)
},
{
default: () => '赠送/比例',
}
),
[[vPerms, ['/admin/goods/process']]]
),
]
}
return el
@@ -109,9 +393,9 @@ const columns = ref([
const data = ref([])
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
itamCount: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -131,24 +415,47 @@ const getList = async () => {
loading.value = true
try {
const res = await api.getHotlist({
pageNum: pagination.value.current,
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
console.log(res)
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
}
loading.value = false
}
const veeify = async (key, row) => {
const res = await api.getHotStatus({
gid: row.gid,
status: key,
})
console.log(res)
getList()
const clear = () => {
isNoteModel.value = false
isDzModel.value = false
notesVal.value = ''
nowRow.value = {}
}
const veeify = async () => {
let data = {}
if (nowKey.value === 1 || nowKey.value === 2) {
data = {
gid: nowRow.value.gid,
status: nowKey.value,
notes: notesVal.value,
}
await api.getHotStatus(data)
await getList()
clear()
} else {
formRef.value?.validate(async (errors) => {
if (!errors) {
data = {
...nowRow.value,
}
await api.getHotStatus(data)
await getList()
clear()
}
})
}
}
</script>

View File

@@ -2,5 +2,5 @@ import { request } from '@/utils'
export default {
getPointlist: (data) => request.post('/point/goods', data),
getPointStatus: (data) => request.post('/point/goods/process', data),
setPointStatus: (data) => request.post('/point/goods/process', data),
}

View File

@@ -6,34 +6,122 @@
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<!-- 拒绝 -->
<n-modal v-model:show="isNoteModel">
<n-card
style="width: 500px"
title="拒绝信息"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-input v-model:value="notesVal" type="textarea" placeholder="请输入拒绝理由...." />
<div m-auto p-10>
<n-button type="primary" @click="veeify">确定</n-button>
<n-button ml-10 @click="clear">取消</n-button>
</div>
</n-card>
</n-modal>
<!-- 商品详情 -->
<n-drawer v-model:show="showDrawer" :width="502">
<n-drawer-content title="商品详情" closable>
<n-space vertical>
<div>
<span>商品分类</span>
<span>{{ goodInfo.class_name }}</span>
</div>
<div>
<span>商品名称</span>
<span>{{ goodInfo.name }}</span>
</div>
<div flex items-center>
<span>封面</span>
<n-image width="100" :src="goodInfo.cover" />
</div>
<div flex items-center>
<span w-90>轮播图</span>
<div flex flex-wrap>
<n-image
v-for="(url, index) in goodInfo.rotation?.split(',')"
:key="index"
width="100"
:src="url"
/>
</div>
</div>
<div>
<span>商品价格</span>
<span>{{ goodInfo.number }}</span>
</div>
<div>
<span>市场价格</span>
<span>{{ goodInfo.market_num }}</span>
</div>
<div>
<span>商品库存</span>
<span>{{ goodInfo.stock }}</span>
</div>
<div>
<span>商品简介</span>
<span>{{ goodInfo.profile }}</span>
</div>
<div>
<span>商品详情</span>
<div v-html="goodInfo.details"></div>
</div>
</n-space>
</n-drawer-content>
</n-drawer>
</CommonPage>
</template>
<script setup>
import api from './api'
import { NDropdown, NButton } from 'naive-ui'
import { h } from 'vue'
import { NEllipsis, NButton, NImage, NSpace } from 'naive-ui'
import { h, withDirectives, resolveDirective } from 'vue'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const isNoteModel = ref(false)
const goodInfo = ref({})
const showDrawer = ref(false)
const notesVal = ref('')
const nowRow = ref({})
const nowKey = ref(null)
const columns = ref([
{
title: '商品名称',
key: 'name',
slot: 'name',
align: 'center',
render: (row) => {
return h(
NEllipsis,
{
style: 'max-width: 200px',
},
{
default: () => row.name,
}
)
},
},
{
title: '商品封面',
slot: 'cover',
align: 'center',
render(row) {
return h('img', {
return h(NImage, {
src: row.cover,
style: {
width: '30px',
height: '30px',
},
width: '30',
})
},
},
@@ -57,50 +145,81 @@ const columns = ref([
slot: 'status',
align: 'center',
render(row) {
return row.status === 0 ? '待审核' : row.status === 1 ? '已审核' : '已拒绝'
return row.status === 3 ? '待审核' : row.status === 1 ? '已审核' : '已拒绝'
},
},
{
title: '备注',
key: 'notes',
align: 'center',
},
{
title: '操作',
slot: 'action',
align: 'center',
render(row) {
const el = []
if (row.status === 0) {
el.push(
if (row.status === 3) {
return [
h(
NDropdown,
NSpace,
{
trigger: 'click',
options: [
{
label: '审核',
key: 1,
},
{
label: '拒绝',
key: 2,
},
],
onSelect: (key) => {
veeify(key, row)
},
justify: 'center',
},
() =>
h(
NButton,
{
type: 'primary',
text: true,
},
() => '审核'
)
)
)
{
default: () => [
withDirectives(
h(
NButton,
{
type: 'primary',
text: true,
onClick: () => {
nowKey.value = 1
nowRow.value = { ...row }
veeify()
},
},
() => '审核通过'
),
[[vPerms, ['/admin/point/goods/process']]]
),
withDirectives(
h(
NButton,
{
type: 'error',
text: true,
onClick: () => {
nowKey.value = 2
nowRow.value = { ...row }
isNoteModel.value = true
},
},
() => '审核拒绝'
),
[[vPerms, ['/admin/point/goods/process']]]
),
withDirectives(
h(
NButton,
{
type: 'info',
text: true,
onClick: () => {
nowKey.value = 3
goodInfo.value = { ...row }
showDrawer.value = true
},
},
() => '商品详情'
),
[[vPerms, ['/admin/point/goods/process']]]
),
],
}
),
]
}
return el
},
},
])
@@ -108,9 +227,9 @@ const columns = ref([
const data = ref([])
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
total: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -130,25 +249,30 @@ const getList = async () => {
loading.value = true
try {
const res = await api.getPointlist({
pageNum: pagination.value.current,
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
console.log(res)
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
}
loading.value = false
}
const veeify = async (key, row) => {
console.log(typeof key)
const res = await api.getPointStatus({
gid: row.gid,
status: key,
const clear = () => {
isNoteModel.value = false
notesVal.value = ''
}
const veeify = async () => {
await api.setPointStatus({
gid: nowRow.value.gid,
status: nowKey.value,
notes: notesVal.value,
})
console.log(res)
getList()
clear()
}
</script>

View File

@@ -5,17 +5,12 @@ export default {
path: '/commodity',
component: Layout,
redirect: '/commodity_class',
meta: {
title: '商品管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'CommodityClass',
path: 'commodity_class',
component: () => import('./commodity_class/index.vue'),
meta: {
title: '商品分类',
icon: 'mdi:account-multiple',
order: 10,
},
},
{
name: 'HotList',
path: 'hot_list',
@@ -26,15 +21,15 @@ export default {
order: 10,
},
},
{
name: 'PointList',
path: 'point_list',
component: () => import('./point/index.vue'),
meta: {
title: '积分商品',
icon: 'mdi:account-multiple',
order: 10,
},
},
// {
// name: 'PointList',
// path: 'point_list',
// component: () => import('./point/index.vue'),
// meta: {
// title: '积分商品',
// icon: 'mdi:account-multiple',
// order: 10,
// },
// },
],
}

View File

@@ -1,100 +0,0 @@
<template>
<CommonPage show-footer>
<n-space size="large">
<n-card title="按钮 Button">
<n-space>
<n-button>Default</n-button>
<n-button type="tertiary">Tertiary</n-button>
<n-button type="primary">Primary</n-button>
<n-button type="info">Info</n-button>
<n-button type="success">Success</n-button>
<n-button type="warning">Warning</n-button>
<n-button type="error">Error</n-button>
</n-space>
</n-card>
<n-card title="带 Icon 的按钮">
<n-space>
<n-button type="info">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
新增
</n-button>
<n-button type="error">
<TheIcon icon="material-symbols:delete-outline" :size="18" class="mr-5" />
删除
</n-button>
<n-button type="warning">
<TheIcon icon="material-symbols:edit-outline" :size="18" class="mr-5" />
编辑
</n-button>
<n-button type="primary">
<TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" />
查看
</n-button>
</n-space>
</n-card>
</n-space>
<n-space size="large" mt-30>
<n-card min-w-340 title="通知 Notification">
<n-space>
<n-button @click="notify('info')">信息</n-button>
<n-button @click="notify('success')">成功</n-button>
<n-button @click="notify('warning')">警告</n-button>
<n-button @click="notify('error')">错误</n-button>
</n-space>
</n-card>
<n-card min-w-340 title="确认弹窗 Dialog">
<n-button type="error" @click="handleDelete">
<icon-mi:delete mr-5 />
删除
</n-button>
</n-card>
<n-card min-w-340 title="消息提醒 Message">
<n-button :loading="loading" type="primary" @click="handleLogin">
<icon-mdi:login v-show="!loading" mr-5 />
登陆
</n-button>
</n-card>
</n-space>
</CommonPage>
</template>
<script setup>
const handleDelete = function () {
$dialog.confirm({
content: '确认删除?',
confirm() {
$message.success('删除成功')
},
cancel() {
$message.warning('已取消')
},
})
}
const loading = ref(false)
function handleLogin() {
loading.value = true
$message.loading('登陆中...')
setTimeout(() => {
$message.error('登陆失败')
$message.loading('正在尝试重新登陆...')
setTimeout(() => {
$message.success('登陆成功')
loading.value = false
}, 2000)
}, 2000)
}
function notify(type) {
$notification[type]({
content: '说点啥呢',
meta: '想不出来',
duration: 2500,
keepAliveOnHover: true,
})
}
</script>

View File

@@ -1,31 +0,0 @@
<template>
<CommonPage show-footer>
<div w-350>
<n-input v-model:value="inputVal" />
<n-input-number v-model:value="number" mt-30 />
<p mt-20 text-center text-14 color-gray>右击标签重新加载可重置keep-alive</p>
</div>
</CommonPage>
</template>
<script setup>
defineOptions({ name: 'KeepAlive' })
const inputVal = ref('')
const number = ref(0)
onMounted(() => {
$message.success('onMounted')
})
onUnmounted(() => {
$message.error('onUnmounted')
})
onActivated(() => {
$message.info('onActivated')
})
onDeactivated(() => {
$message.warning('onDeactivated')
})
</script>

View File

@@ -1,44 +0,0 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: 'Test',
path: '/base',
component: Layout,
redirect: '/base/index',
isHidden: true,
meta: {
title: '基础功能',
icon: 'majesticons:compass-line',
order: 1,
},
children: [
{
name: 'BaseComponents',
path: 'index',
component: () => import('./index.vue'),
meta: {
title: '基础组件',
icon: 'material-symbols:auto-awesome-outline-rounded',
},
},
{
name: 'Unocss',
path: 'unocss',
component: () => import('./unocss/index.vue'),
meta: {
title: 'Unocss',
icon: 'material-symbols:auto-awesome-outline-rounded',
},
},
{
name: 'KeepAlive',
path: 'keep-alive',
component: () => import('./keep-alive/index.vue'),
meta: {
title: 'KeepAlive',
icon: 'material-symbols:auto-awesome-outline-rounded',
keepAlive: true,
},
},
],
}

View File

@@ -1,69 +0,0 @@
<template>
<CommonPage show-footer>
<p>
文档
<a c-blue hover-decoration-underline href="https://uno.antfu.me/" target="_blank">
https://uno.antfu.me/
</a>
</p>
<p>
playground
<a c-blue hover-decoration-underline href="https://unocss.antfu.me/play/" target="_blank">
https://unocss.antfu.me/play/
</a>
</p>
<div mt-20 w-350 f-c-c flex-col>
<div flex flex-wrap justify-around rounded-10 p-10 border="1 solid #ccc">
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
</div>
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 self-center rounded-3 bg-black dark:bg-white />
<span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
</div>
<div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<div flex-col justify-between>
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
<div flex-col justify-between>
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
</div>
<div m-20 h-50 w-50 flex-col items-center justify-between rounded-5 p-10 border="1 solid">
<div w-full flex justify-between>
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
<div h-6 w-6 rounded-3 bg-black dark:bg-white />
<div w-full flex justify-between>
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
</div>
<div m-20 h-50 w-50 flex-col justify-between rounded-5 p-10 border="1 solid">
<div w-full flex justify-between>
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
<div w-full flex justify-between>
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
<div w-full flex justify-between>
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div>
</div>
</div>
<h2 mt-10 text-14 font-normal color-gray>Flex 骰子</h2>
</div>
</CommonPage>
</template>

View File

@@ -1,47 +0,0 @@
<template>
<CommonPage>
<div h-60 flex items-center bg-white pl-20 pr-20 dark:bg-dark>
<input
v-model="post.title"
class="mr-20 flex-1 pb-15 pt-15 text-20 font-bold color-primary"
dark:bg-dark
type="text"
placeholder="输入文章标题..."
/>
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">
<TheIcon v-if="!btnLoading" icon="line-md:confirm-circle" class="mr-5" :size="18" />
保存
</n-button>
</div>
<MdEditor v-model="post.content" style="height: calc(100vh - 305px)" dark:bg-dark />
</CommonPage>
</template>
<script setup>
import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
defineOptions({ name: 'MDEditor' })
// refs
let post = ref({})
let btnLoading = ref(false)
function handleSavePost() {
btnLoading.value = true
$message.loading('正在保存...')
setTimeout(() => {
$message.success('保存成功')
btnLoading.value = false
}, 2000)
}
</script>
<style lang="scss">
.md-preview {
ul,
ol {
list-style: revert;
}
}
</style>

View File

@@ -1,46 +0,0 @@
<template>
<AppPage>
<div class="h-full flex-col" border="1 solid #ccc" dark:bg-dark>
<WangToolbar
border-b="1px solid #ccc"
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
/>
<WangEditor
v-model="valueHtml"
style="flex: 1; overflow-y: hidden"
:default-config="editorConfig"
mode="default"
@on-created="handleCreated"
/>
</div>
</AppPage>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css'
import { Editor as WangEditor, Toolbar as WangToolbar } from '@wangeditor/editor-for-vue'
defineOptions({ name: 'RichTextEditor' })
const editorRef = shallowRef()
const toolbarConfig = { excludeKeys: 'fullScreen' }
const editorConfig = { placeholder: '请输入内容...', MENU_CONF: {} }
const valueHtml = ref('')
const handleCreated = (editor) => {
editorRef.value = editor
}
</script>
<style>
html.dark {
--w-e-textarea-bg-color: #333;
--w-e-textarea-color: #fff;
--w-e-toolbar-bg-color: #333;
--w-e-toolbar-color: #fff;
--w-e-toolbar-active-bg-color: #666;
--w-e-toolbar-active-color: #fff;
/* ...其他... */
}
</style>

View File

@@ -1,65 +0,0 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: 'Demo',
path: '/demo',
component: Layout,
redirect: '/demo/crud',
meta: {
title: '示例页面',
customIcon: 'logo',
role: ['admin'],
requireAuth: true,
order: 3,
},
children: [
{
name: 'Crud',
path: 'crud',
component: () => import('./table/index.vue'),
meta: {
title: 'CRUD表格',
icon: 'ic:baseline-table-view',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
{
name: 'MDEditor',
path: 'md-editor',
component: () => import('./editor/md-editor.vue'),
meta: {
title: 'MD编辑器',
icon: 'ri:markdown-line',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
{
name: 'RichTextEditor',
path: 'rich-text',
component: () => import('./editor/rich-text.vue'),
meta: {
title: '富文本编辑器',
icon: 'ic:sharp-text-rotation-none',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
{
name: 'Upload',
path: 'upload',
component: () => import('./upload/index.vue'),
meta: {
title: '图片上传',
icon: 'mdi:upload',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
],
}

View File

@@ -1,9 +0,0 @@
import { request } from '@/utils'
export default {
getPosts: (params = {}) => request.get('posts', { params }),
getPostById: (id) => request.get(`/post/${id}`),
addPost: (data) => request.post('/post', data),
updatePost: (data) => request.put(`/post/${data.id}`, data),
deletePost: (id) => request.delete(`/post/${id}`),
}

View File

@@ -1,233 +0,0 @@
<template>
<CommonPage show-footer title="文章">
<template #action>
<div>
<n-button type="primary" secondary @click="$table?.handleExport()">
<TheIcon icon="mdi:download" :size="18" class="mr-5" />
导出
</n-button>
<n-button type="primary" class="ml-16" @click="handleAdd">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
新建文章
</n-button>
</div>
</template>
<CrudTable
ref="$table"
v-model:query-items="queryItems"
:extra-params="extraParams"
:scroll-x="1200"
:columns="columns"
:get-data="api.getPosts"
@on-checked="onChecked"
@on-data-change="(data) => (tableData = data)"
>
<template #queryBar>
<QueryBarItem label="标题" :label-width="50">
<n-input
v-model:value="queryItems.title"
type="text"
placeholder="请输入标题"
@keypress.enter="$table?.handleSearch"
/>
</QueryBarItem>
</template>
</CrudTable>
<!-- 新增/编辑/查看 -->
<CrudModal
v-model:visible="modalVisible"
:title="modalTitle"
:loading="modalLoading"
:show-footer="modalAction !== 'view'"
@on-save="handleSave"
>
<n-form
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="80"
:model="modalForm"
:disabled="modalAction === 'view'"
>
<n-form-item label="作者" path="author">
<n-input v-model:value="modalForm.author" disabled />
</n-form-item>
<n-form-item
label="文章标题"
path="title"
:rule="{
required: true,
message: '请输入文章标题',
trigger: ['input', 'blur'],
}"
>
<n-input v-model:value="modalForm.title" placeholder="请输入文章标题" />
</n-form-item>
<n-form-item
label="文章内容"
path="content"
:rule="{
required: true,
message: '请输入文章内容',
trigger: ['input', 'blur'],
}"
>
<n-input
v-model:value="modalForm.content"
placeholder="请输入文章内容"
type="textarea"
:autosize="{
minRows: 3,
maxRows: 5,
}"
/>
</n-form-item>
</n-form>
</CrudModal>
</CommonPage>
</template>
<script setup>
import { NButton, NSwitch } from 'naive-ui'
import { formatDateTime, renderIcon, isNullOrUndef } from '@/utils'
import { useCRUD } from '@/composables'
import api from './api'
defineOptions({ name: 'Crud' })
const $table = ref(null)
/** 表格数据,触发搜索的时候会更新这个值 */
const tableData = ref([])
/** QueryBar筛选参数可选 */
const queryItems = ref({})
/** 补充参数(可选) */
const extraParams = ref({})
onActivated(() => {
$table.value?.handleSearch()
})
const columns = [
{ type: 'selection', fixed: 'left' },
{
title: '发布',
key: 'isPublish',
width: 60,
align: 'center',
fixed: 'left',
render(row) {
return h(NSwitch, {
size: 'small',
rubberBand: false,
value: row['isPublish'],
loading: !!row.publishing,
onUpdateValue: () => handlePublish(row),
})
},
},
{ title: '标题', key: 'title', width: 150, ellipsis: { tooltip: true } },
{ title: '分类', key: 'category', width: 80, ellipsis: { tooltip: true } },
{ title: '创建人', key: 'author', width: 80 },
{
title: '创建时间',
key: 'createDate',
width: 150,
render(row) {
return h('span', formatDateTime(row['createDate']))
},
},
{
title: '最后更新时间',
key: 'updateDate',
width: 150,
render(row) {
return h('span', formatDateTime(row['updateDate']))
},
},
{
title: '操作',
key: 'actions',
width: 240,
align: 'center',
fixed: 'right',
hideInExcel: true,
render(row) {
return [
h(
NButton,
{
size: 'small',
type: 'primary',
secondary: true,
onClick: () => handleView(row),
},
{ default: () => '查看', icon: renderIcon('majesticons:eye-line', { size: 14 }) }
),
h(
NButton,
{
size: 'small',
type: 'primary',
style: 'margin-left: 15px;',
onClick: () => handleEdit(row),
},
{ default: () => '编辑', icon: renderIcon('material-symbols:edit-outline', { size: 14 }) }
),
h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-left: 15px;',
onClick: () => handleDelete(row.id),
},
{
default: () => '删除',
icon: renderIcon('material-symbols:delete-outline', { size: 14 }),
}
),
]
},
},
]
// 选中事件
function onChecked(rowKeys) {
if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`)
}
// 发布
function handlePublish(row) {
if (isNullOrUndef(row.id)) return
row.publishing = true
setTimeout(() => {
row.isPublish = !row.isPublish
row.publishing = false
$message?.success(row.isPublish ? '已发布' : '已取消发布')
}, 1000)
}
const {
modalVisible,
modalAction,
modalTitle,
modalLoading,
handleAdd,
handleDelete,
handleEdit,
handleView,
handleSave,
modalForm,
modalFormRef,
} = useCRUD({
name: '文章',
initForm: { author: '大脸怪' },
doCreate: api.addPost,
doDelete: api.deletePost,
doUpdate: api.updatePost,
refresh: () => $table.value?.handleSearch(),
})
</script>

View File

@@ -1,84 +0,0 @@
<template>
<CommonPage>
<n-upload
class="mx-auto w-[75%] p-20 text-center"
:custom-request="handleUpload"
:show-file-list="false"
accept=".png,.jpg,.jpeg"
@before-upload="onBeforeUpload"
>
<n-upload-dragger>
<div class="h-150 f-c-c flex-col">
<TheIcon icon="mdi:upload" :size="68" class="mb-12 c-primary" />
<n-text class="text-14 c-gray">点击或者拖动文件到该区域来上传</n-text>
</div>
</n-upload-dragger>
</n-upload>
<n-card v-if="imgList && imgList.length" class="mt-16 items-center">
<n-image-group>
<n-space justify="space-between" align="center">
<n-card v-for="(item, index) in imgList" :key="index" class="w-280 hover:card-shadow">
<div class="h-160 f-c-c">
<n-image width="200" :src="item.url" />
</div>
<n-space class="mt-16" justify="space-evenly">
<n-button dashed type="primary" @click="copy(item.url)">url</n-button>
<n-button dashed type="primary" @click="copy(`![${item.fileName}](${item.url})`)">
MD
</n-button>
<n-button
dashed
type="primary"
@click="copy(`&lt;img src=&quot;${item.url}&quot; /&gt;`)"
>
img
</n-button>
</n-space>
</n-card>
<div v-for="i in 4" :key="i" class="w-280" />
</n-space>
</n-image-group>
</n-card>
</CommonPage>
</template>
<script setup>
import { useClipboard } from '@vueuse/core'
defineOptions({ name: 'Upload' })
const { copy, copied } = useClipboard()
const imgList = reactive([
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.qszone.com/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
])
watch(copied, (val) => {
val && $message.success('已复制到剪切板')
})
function onBeforeUpload({ file }) {
if (!file.file?.type.startsWith('image/')) {
$message.error('只能上传图片')
return false
}
return true
}
async function handleUpload({ file, onFinish }) {
if (!file || !file.type) {
$message.error('请选择文件')
}
// 模拟上传
$message.loading('上传中...')
setTimeout(() => {
$message.success('上传成功')
imgList.push({ fileName: file.name, url: URL.createObjectURL(file.file) })
onFinish()
}, 1500)
}
</script>

7
src/views/finance/api.js Normal file
View File

@@ -0,0 +1,7 @@
import { request } from '@/utils'
export default {
getData: (data) => request.post('/store/withdraw', data),
// 审核提现
passAudit: (data) => request.post('/store/withdraw/edit', data),
}

358
src/views/finance/index.vue Normal file
View File

@@ -0,0 +1,358 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-row gutter="12">
<n-col :span="24">
<div flex>
<n-card w-400>
<n-statistic label="总提现金额(含已驳回)" tabular-nums>
<n-number-animation
ref="numberAnimationInstRef"
:from="0"
:to="cardData.total / 100"
/>
</n-statistic>
</n-card>
<n-card ml-10 w-400>
<n-statistic label="待处理金额" tabular-nums>
<n-number-animation
ref="numberAnimationInstRef"
:from="0"
:to="cardData.service / 100"
/>
</n-statistic>
</n-card>
<n-card ml-10 w-400>
<n-statistic label="已审核金额" tabular-nums>
<n-number-animation
ref="numberAnimationInstRef"
:from="0"
:to="cardData.count / 100"
/>
</n-statistic>
</n-card>
</div>
</n-col>
<n-col :span="24">
<div mt-10>
<span w-80>提现状态:</span>
<n-radio-group v-model:value="queryData.status" class="ml-10">
<n-radio-button
v-for="song in songs"
:key="song.value"
:value="song.value"
:label="song.label"
/>
</n-radio-group>
</div>
</n-col>
<n-col :span="24">
<div mt-10 flex items-center>
<span w-80>号码搜索:</span>
<n-input v-model:value="queryData.word" style="width: 25%" placeholder="请输入手机号码" />
</div>
</n-col>
<n-col :span="10">
<div mt-10 flex items-center>
<span w-80>申请时间:</span>
<n-date-picker
v-model:formatted-value="queryData.time"
value-format="yyyy-MM-dd"
type="daterange"
clearable
/>
</div>
</n-col>
<n-col :span="24">
<div mt-10>
<n-button type="primary" @click="getList">搜索</n-button>
<n-button ml-10 @click="clearQueryData">重置</n-button>
</div>
</n-col>
</n-row>
<n-data-table
class="mt-10"
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<!-- 打款记录 -->
<n-modal v-model:show="showModal">
<n-card
style="width: 500px"
title="打款记录"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
<n-grid :cols="1" :x-gap="24">
<n-form-item-gi :span="12" label="打款截图" path="img">
<Upload v-model:list="model.img" />
</n-form-item-gi>
<n-form-item-gi>
<div w-full flex justify-center>
<n-button type="primary" @click="ok">确定</n-button>
<n-button class="ml-10" @click="clear">关闭</n-button>
</div>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import api from './api'
import { NButton, NImage, NTag } from 'naive-ui'
import Upload from '@/components/Upload.vue'
import { h, withDirectives, resolveDirective } from 'vue'
const vPerms = resolveDirective('perms')
const vRole = [[vPerms, ['/admin/store/withdraw/edit']]]
const showModal = ref(false)
const queryData = ref({
status: '',
word: '',
time: '',
})
const cardData = ref({
total: 0,
service: 0,
count: 0,
})
const songs = ref([
{
value: 1,
label: '已审核',
},
{
value: 2,
label: '已驳回',
},
{
value: 3,
label: '待审核',
},
])
const nowRow = ref({})
const formRef = ref(null)
const model = ref({
img: [],
})
const rules = {
img: { required: true, type: 'array', message: '请上传打款截图' },
}
const columns = ref([
{
title: '名字',
key: 'name',
align: 'center',
},
{
title: '电话',
key: 'phone',
align: 'center',
},
{
title: '银行名称',
key: 'bank',
align: 'center',
},
{
title: '银行卡号',
key: 'bank_card',
align: 'center',
},
{
title: '账户名称',
key: 'bank_name',
align: 'center',
},
{
title: '法人',
key: 'bank_user',
align: 'center',
},
{
title: '提现金额',
slot: 'integral',
align: 'center',
render: (row) => {
return h('span', {}, row.integral / 100)
},
},
{
title: '审核状态',
align: 'center',
slot: 'status',
render: (row) => {
return h(
NTag,
{
type: row.status === 1 ? 'success' : row.status === 2 ? 'error' : 'warning',
},
{
default: () => (row.status === 1 ? '已审核' : row.status === 2 ? '已驳回' : '待审核'),
}
)
},
},
{
title: '申请时间',
key: 'add_time',
align: 'center',
},
{
title: '打款截图',
slot: 'img',
render: (row) => {
return h(NImage, {
src: row.status_img,
width: '50',
})
},
},
{
title: '操作',
align: 'center',
slot: 'action',
render: (row) => {
if (row.status === 3) {
return [
withDirectives(
h(
NButton,
{
text: true,
type: 'primary',
onClick: () => {
nowRow.value = row
showModal.value = true
},
},
{
default: () => '审核',
}
),
vRole
),
withDirectives(
h(
NButton,
{
class: 'ml-10',
text: true,
type: 'error',
onClick: () => {
nowRow.value = row
refuse()
},
},
{
default: () => '拒绝',
}
),
vRole
),
]
}
},
},
])
const loading = ref(false)
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
})
onMounted(() => {
getList()
})
const getList = async () => {
loading.value = true
const query_data = {
Status: queryData.value.status || '',
Phone: queryData.value.word || '',
StartTime: queryData.value.time === null ? '' : queryData.value.time[0] || '',
EndTime: queryData.value.time === null ? '' : queryData.value.time[1] || '',
}
const res = await api.getData({
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
...query_data,
})
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
cardData.value.total = res.data.all
cardData.value.service = res.data.audit_integral
cardData.value.count = res.data.success_integral
loading.value = false
}
const clear = async () => {
model.value = {
img: [],
}
showModal.value = false
formRef.value?.restoreValidation()
await getList()
}
const ok = () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
const res = await api.passAudit({
bid: nowRow.value.bid,
wid: nowRow.value.wid,
img: model.value.img[0].url,
status: 1,
})
$message.success(res.msg)
clear()
}
})
}
const refuse = async () => {
const res = await api.passAudit({
bid: nowRow.value.bid,
wid: nowRow.value.wid,
img: model.value.img[0]?.url || '',
status: 2,
})
clear()
$message.success(res.msg)
}
const clearQueryData = () => {
queryData.value = {
status: '',
time: null,
word: '',
}
getList()
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,25 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: '财务管理',
path: '/finance',
component: Layout,
redirect: '/finance_list',
meta: {
title: '财务管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'financelist',
path: 'finance_list',
component: () => import('./index.vue'),
meta: {
title: '商户提现',
icon: 'mdi:account-multiple',
order: 10,
},
},
],
}

View File

@@ -1,8 +1,43 @@
import { request } from '@/utils'
export default {
getData: () => request.post('/dice/getisStart'),
setStatus: (data) => request.post('/dice/isStart', data),
getDS: () => request.post('/dice/getBetting'),
setDS: (data) => request.post('/dice/setBetting', data),
getData: () => request.post('/getisStart'),
setStatus: (data) => request.post('/isStart', data),
getDS: () => request.post('/getBetting'),
setDS: (data) => request.post('/setBetting', data),
getKJList: () => request.post('/draw'),
// 获取统计
getStatistics: (data) => request.post('/user/betting/list', data),
// log
getLog: () => request.post('/log'),
// 宙斯详情
getDetail: (data) => request.post('/log/betting/list', data),
// 获取游戏大厅
getGameList: (data) => request.post('/game/list', data),
// 添加修改游戏
addGame: (data) => request.post('/game/edit', data),
// 当前投注用户
nowUser: () => request.post('/now/draw/user', {}),
// 全部投注用户
allUser: () => request.post('/all/draw/user', {}),
// 吹气球相关
// 游戏状态
getisBalloonStart: () => request.post('/getisBalloonStart'),
// 修改游戏状态
setisBalloonStart: (data) => request.post('/isBalloonStart', data),
// 全部开奖记录
getBalloonList: () => request.post('/balloon/draw'),
// 本局投注记录
getBalloonUser: () => request.post('/now/balloon/draw/user'),
// 全部投注记录
getAllBalloonUser: () => request.post('/all/balloon/draw/user'),
// 统计全部投注和中奖列表
getUserList: () => request.post('/user/balloon/list'),
}

View File

@@ -0,0 +1,206 @@
<template>
<CommonPage show-footer :title="$route.title">
<div flex items-center>
<div mr-20 flex>
<div>游戏状态</div>
<n-switch
v-model:value="gameStatus"
:checked-value="1"
:unchecked-value="2"
@update:value="handleUpdateValue"
/>
</div>
<div ml-20 flex items-center>
<div>本局记录</div>
<n-button type="primary" @click="openModal(1)">预览</n-button>
</div>
<div ml-20 flex items-center>
<div>全部记录</div>
<n-button type="primary" @click="openModal(2)">预览</n-button>
</div>
</div>
<n-modal v-model:show="showModal">
<n-card
style="width: 900px"
:title="modalTitle"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template v-if="keyModal === 1">
<div>
<span>
总投注(豆子):
<span text-red>{{ nowData.total_number }}</span>
</span>
<span ml-20>
总积分:
<span text-red>{{ nowData.total_integral }}</span>
</span>
</div>
<n-data-table
:loading="nowData.Loading"
:columns="nowData.Columns"
:data="nowData.data"
:max-height="700"
:pagination="false"
:bordered="false"
remote
@update:sorter="nowData.handleSorterChange"
/>
</template>
<template v-else>
<div>
<span>
总投注(豆子):
<span text-red>{{ allData.total_number }}</span>
</span>
<span ml-20>
总积分:
<span text-red>{{ allData.total_integral }}</span>
</span>
</div>
<n-data-table
:loading="allData.Loading"
:columns="allData.Columns"
:data="allData.data"
:max-height="700"
:pagination="false"
:bordered="false"
remote
@update:sorter="allData.handleSorterChange"
/>
</template>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import api from '../../api'
const gameStatus = ref(2)
onMounted(() => {
get_status()
})
const get_status = async () => {
const res = await api.getisBalloonStart()
gameStatus.value = res.data.balloonStart
}
const handleUpdateValue = async (e) => {
await api.setisBalloonStart({
Start: e,
})
$message.success('修改成功')
get_status()
}
const showModal = ref(false)
const keyModal = ref(null)
const modalTitle = ref('')
const openModal = (type) => {
keyModal.value = type
showModal.value = true
modalTitle.value = type === 1 ? '本局记录' : '全部记录'
if (type === 1) return fetchData(nowData)
fetchData(allData)
}
const { value: tempCol } = ref([
{
title: '昵称',
key: 'User',
align: 'center',
},
{
title: '电话',
key: 'Phone',
align: 'center',
},
{
title: '期数',
key: 'Periods',
align: 'center',
},
{
title: '下注豆子',
key: 'TotalCount',
align: 'center',
sorter: true,
sortOrder: false,
},
{
title: '赢积分',
key: 'NumberSum',
align: 'center',
sorter: true,
sortOrder: false,
},
{
title: '购买秒数',
key: 'DrawTime',
align: 'center',
},
])
const { value: nowData } = ref({
Loading: false,
Columns: [...tempCol],
data: [],
total_number: 0,
total_integral: 0,
handleSorterChange: (sorter) => sortData(sorter, nowData),
api: api.getBalloonUser,
})
const { value: allData } = ref({
Loading: false,
Columns: [...tempCol],
data: [],
total_number: 0,
total_integral: 0,
handleSorterChange: (sorter) => sortData(sorter, allData),
api: api.getAllBalloonUser,
})
const fetchData = async (target) => {
target.Loading = true
const res = await target.api()
target.data = res.data.data || []
target.total_integral = res.data.total_integral
target.total_number = res.data.total_number
target.Loading = false
}
const sortData = (sorter, target) => {
if (!target.Loading) {
target.Loading = true
switch (sorter.columnKey) {
case 'TotalCount':
target.Columns[3].sortOrder = !sorter ? false : sorter.order
target.data = target.data.sort((a, b) => {
if (sorter.order === 'descend') return b.TotalCount - a.TotalCount
return a.TotalCount - b.TotalCount
})
break
case 'NumberSum':
target.Columns[4].sortOrder = !sorter ? false : sorter.order
target.data = target.data.sort((a, b) => {
if (sorter.order === 'descend') return b.NumberSum - a.NumberSum
return a.NumberSum - b.NumberSum
})
break
}
target.Loading = false
}
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,214 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-grid class="mb-10" x-gap="12" :cols="4">
<n-gi>
<n-date-picker
v-model:formatted-value="range"
value-format="yyyy-MM-dd"
type="daterange"
clearable
/>
</n-gi>
<n-gi>
<n-button type="primary" @click="getList">搜索</n-button>
<n-button ml-10 @click="clear">重置</n-button>
</n-gi>
</n-grid>
<div w-full flex items-center>
<Echarts :loading="loading" :option="option" />
<Echarts :loading="loading" :option="option1" />
</div>
<div w-full flex items-center justify-between>
<n-card title="开奖记录" :bordered="false" content-style="padding: 0;">
<n-data-table
:max-height="500"
:loading="loading"
:columns="columns"
:data="data"
:bordered="true"
:virtual-scroll="true"
remote
/>
</n-card>
</div>
</CommonPage>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import api from '../../api.js'
import Echarts from '@/components/Echarts.vue'
import dayjs from 'dayjs'
const loading = ref(false)
const range = ref(null)
const option = ref({
title: {
text: '单期下注(豆子)/赔付(积分) 统计',
left: 'center',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: [],
},
yAxis: {
type: 'value',
},
series: [
{
name: '下注(豆子)',
data: [],
type: 'bar',
},
{
name: '赔付(积分)',
data: [],
type: 'bar',
},
],
dataZoom: [
{
type: 'inside',
},
{
type: 'slider',
},
],
})
const option1 = ref({
title: {
text: '总下注(豆子)/总赔付(积分)',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'right',
},
series: [
{
type: 'pie',
radius: '50%',
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
})
const data = ref([])
const columns = ref([
{
title: '期数',
key: 'Periods',
align: 'center',
},
{
title: '开奖秒数',
key: 'Name',
align: 'center',
},
{
title: '下注',
key: 'NumberSum',
align: 'center',
},
{
title: '赔付',
key: 'TotalCount',
align: 'center',
},
{
title: '时间',
key: 'Date',
align: 'center',
},
])
onMounted(() => {
getList()
})
const clear = () => {
range.value = null
getList()
}
const getList = async () => {
loading.value = true
const dataObj = {
StartTime: dayjs().format('YYYY-MM-DD'),
EndTime: dayjs().format('YYYY-MM-DD'),
}
if (range.value) {
dataObj.StartTime = range.value[0]
dataObj.EndTime = range.value[1]
}
const res = await api.getUserList(dataObj)
const newData = res.data.data || []
data.value = newData
option.value.xAxis.data = []
option.value.series[0].data = []
option.value.series[1].data = []
option1.value.series[0].data = []
if (newData.length > 0) {
res.data.data.forEach((item) => {
const a = (
((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) *
100
).toFixed(2)
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
2
)
option.value.xAxis.data.push(`${item.Periods}期-${item.Name}`)
option.value.series[0].name = `下注(豆子): ${a}%`
option.value.series[0].data.push(item.NumberSum * 10)
option.value.series[1].name = `赔付(积分): ${b}%`
option.value.series[1].data.push(item.TotalCount)
})
const a = (((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
2
)
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(2)
option1.value.series[0].data.push({ value: res.data.total * 10, name: `总下注: ${a}%` })
option1.value.series[0].data.push({
value: res.data.totalDices,
name: `总赔付: ${b}%`,
})
}
loading.value = false
}
</script>
<style lang="scss" scoped>
.chart {
width: 50%;
height: 400px;
}
</style>

View File

@@ -1,124 +0,0 @@
<template>
<CommonPage show-footer :title="$route.title">
<div flex>
<div flex>
<div>游戏状态</div>
<n-switch
v-model:value="val1"
:checked-value="1"
:unchecked-value="2"
@update:value="handleUpdateValue1"
/>
</div>
<div ml-20 flex>
<div>点杀状态</div>
<n-switch
v-model:value="val"
:checked-value="1"
:unchecked-value="2"
@update:value="handleUpdateValue"
/>
</div>
</div>
<div flex>
<div>
预开期数
<span text-25>{{ list[0]?.periods || 0 }}</span>
</div>
<div ml-20>
剩余开奖时间
<span text-25>{{ time || 0 }}</span>
</div>
</div>
<div flex flex-wrap justify-between>
<n-card
v-for="item in list"
:key="item.ID"
class="mb-10 mt-10 w-300 flex-shrink-0 cursor-pointer"
:title="item.name"
>
<p text-25 op-60 :style="{ color: item.count === 0 ? 'green' : 'red' }">{{ item.count }}</p>
</n-card>
<div h-0 w-300></div>
<div h-0 w-300></div>
</div>
</CommonPage>
</template>
<script setup>
import api from '../api'
const ws = new WebSocket(`wss://${import.meta.env.VITE_WS_URL}`)
const ws1 = new WebSocket(`wss://${import.meta.env.VITE_WS1_URL}`)
const list = ref([])
const val1 = ref(null)
const val = ref(null)
const time = ref(null)
ws.onopen = () => {
console.log('1连接成功')
}
ws.onmessage = (msg) => {
const res = JSON.parse(msg.data)
list.value = res
}
ws1.onopen = () => {
console.log('2连接成功')
}
ws1.onmessage = (msg) => {
const res = JSON.parse(msg.data)
switch (res.code) {
case 200:
// let num = res.data
time.value = res.data
break
case 301:
$message.error(res.msg)
break
}
}
onMounted(() => {
get_data()
get_data1()
})
const get_data = async () => {
const res = await api.getDS()
val.value = res.data.diceStatus
}
const get_data1 = async () => {
const res = await api.getData()
val1.value = res.data.diceStart
}
onBeforeUnmount(() => {
ws.close()
})
const handleUpdateValue = async (e) => {
const res = await api.setDS({
status: e,
})
$message.success(res.msg)
get_data()
}
const handleUpdateValue1 = async (e) => {
const res = await api.setStatus({
start: e,
})
$message.success(res.msg)
get_data1()
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,354 @@
<template>
<CommonPage show-footer :title="$route.title">
<div flex items-center>
<div mr-20 flex>
<div>游戏状态</div>
<n-switch
v-model:value="val1"
:checked-value="1"
:unchecked-value="2"
@update:value="handleUpdateValue1"
/>
</div>
<div flex items-center>
<div>开奖记录</div>
<n-button type="primary" @click="openData">预览</n-button>
</div>
<div ml-20 flex items-center>
<div>本局记录</div>
<n-button type="primary" @click="openJl(1)">预览</n-button>
</div>
<div ml-20 flex items-center>
<div>全部记录</div>
<n-button type="primary" @click="openJl(2)">预览</n-button>
</div>
</div>
<div flex>
<div>
预开期数
<span text-25>{{ list[0]?.Periods || 0 }}</span>
</div>
<div ml-20>
剩余开奖时间
<span text-25>{{ time || 0 }}</span>
</div>
<div ml-20>
本局总下注
<span text-25>{{ totalA || 0 }}</span>
</div>
</div>
<n-spin size="large" :show="show">
<div flex flex-wrap justify-between>
<n-card
v-for="item in list"
:key="item.ID"
class="mb-10 mt-10 h-120 w-250 flex-shrink-0 cursor-pointer"
:title="item.name"
>
<p text-25 op-60 :style="{ color: item.count === 0 ? 'green' : 'red' }">
{{ item.count }}
</p>
</n-card>
<div h-0 w-250></div>
<div h-0 w-250></div>
</div>
</n-spin>
<n-spin size="large" :show="show1">
<div flex flex-wrap justify-between>
<n-card
v-for="item in list1"
:key="item.ID"
class="mb-10 mt-10 h-150 w-250 flex-shrink-0 cursor-pointer"
:title="`${item.NumName}(${item.Name})`"
>
<p text-25 op-60 :style="{ color: item.Total === 0 ? 'green' : 'red' }">
{{ item.Total }}
</p>
<n-popconfirm @positive-click="handleUpdateValue(item.ID)">
<template #trigger>
<n-button>你猜</n-button>
</template>
一切都将一去杳然任何人都无法将其捕获
</n-popconfirm>
</n-card>
<div h-0 w-250></div>
<div h-0 w-250></div>
<div h-0 w-250></div>
<div h-0 w-250></div>
</div>
</n-spin>
<!-- 开奖记录 -->
<n-modal v-model:show="showModal">
<n-card
style="width: 900px"
title="开奖记录"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:max-height="600"
:pagination="false"
:bordered="false"
remote
/>
</n-card>
</n-modal>
<!-- 游戏记录 -->
<n-modal v-model:show="jlModal">
<n-card
style="width: 900px"
:title="jlTitle"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<div>
<span>
总投注(豆子):
<span text-red>{{ jlData.total_number }}</span>
</span>
<span ml-20>
总积分:
<span text-red>{{ jlData.total_integral }}</span>
</span>
</div>
<n-data-table
:loading="jlLoading"
:columns="jlColumns"
:data="jlData.data"
:max-height="600"
:pagination="false"
:bordered="false"
remote
@update:sorter="handleSorterChange"
/>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import { h } from 'vue'
import api from '../../api'
import { getToken } from '@/utils'
const ws = new WebSocket(`wss://${import.meta.env.VITE_WS_URL}`)
const ws1 = new WebSocket(`wss://${import.meta.env.VITE_WS1_URL}`)
const list = ref([])
const list1 = ref([])
const val1 = ref(null)
// const val = ref(null)
const time = ref(null)
const show = ref(true)
const show1 = ref(true)
ws.onopen = () => {
console.log('1连接成功')
}
const totalA = ref(null)
ws.onmessage = (msg) => {
const res = JSON.parse(msg.data)
list.value = res.betting.sort((a, b) => b.Total - a.Total)
show.value = false
list1.value = res.list.sort((a, b) => b.Total - a.Total)
show1.value = false
totalA.value = res.total
}
ws1.onopen = () => {
console.log('2连接成功')
}
ws1.onmessage = (msg) => {
const res = JSON.parse(msg.data)
switch (res.code) {
case 200:
// let num = res.data
time.value = res.data
break
case 301:
$message.error(res.msg)
break
}
}
onMounted(() => {
// get_data()
get_data1()
})
// const get_data = async () => {
// const res = await api.getDS()
// val.value = res.data.diceStatus
// }
const get_data1 = async () => {
const res = await api.getData()
val1.value = res.data.diceStart
}
onBeforeUnmount(() => {
ws.close()
})
const handleUpdateValue = async (e) => {
const res = await api.setDS({
status: 1,
id: e,
user_id: getToken(),
Periods: list.value[0]?.Periods,
})
$message.success(res.msg)
// get_data()
}
const handleUpdateValue1 = async (e) => {
const res = await api.setStatus({
start: e,
})
$message.success(res.msg)
get_data1()
}
const showModal = ref(false)
const loading = ref(false)
const columns = ref([
{
title: '期数',
key: 'Periods',
align: 'center',
},
{
title: '开奖号码',
key: 'Name',
align: 'center',
},
{
title: '开奖时间',
key: 'DrawTime',
align: 'center',
},
{
title: '开奖号码',
slot: 'Num',
align: 'center',
render: (row) => {
return h('p', `${row.Start}-${row.End}`)
},
},
])
const data = ref([])
const openData = async () => {
try {
showModal.value = true
loading.value = true
const res = await api.getKJList()
console.log(res)
data.value = res.data.data
loading.value = false
} catch (error) {
$message.error(error.msg)
throw error
}
}
const jlModal = ref(false)
const jlTitle = ref('')
const jlData = ref({})
const jlColumns = ref([
{
title: '昵称',
key: 'User',
align: 'center',
},
{
title: '电话',
key: 'Phone',
align: 'center',
},
{
title: '期数',
key: 'Periods',
align: 'center',
},
{
title: '下注豆子',
key: 'TotalCount',
align: 'center',
sorter: true,
sortOrder: false,
},
{
title: '赢积分',
key: 'NumberSum',
align: 'center',
sorter: true,
sortOrder: false,
},
{
title: '购买号码',
key: 'PeriodsNum',
align: 'center',
},
{
title: '时间',
key: 'DrawTime',
align: 'center',
},
])
const jlLoading = ref(false)
const openJl = async (e) => {
try {
jlModal.value = true
jlTitle.value = e === 1 ? '本局游戏记录' : '全部游戏记录'
jlLoading.value = true
const res = e === 1 ? await api.nowUser() : await api.allUser()
jlData.value = res.data
} catch (error) {
$message.error(error.msg)
}
jlLoading.value = false
}
const handleSorterChange = (sorter) => {
if (!jlLoading.value) {
jlLoading.value = true
switch (sorter.columnKey) {
case 'TotalCount':
jlColumns.value[3].sortOrder = !sorter ? false : sorter.order
jlData.value.data = jlData.value.data.sort((a, b) => {
if (sorter.order === 'descend') return b.TotalCount - a.TotalCount
return a.TotalCount - b.TotalCount
})
break
case 'NumberSum':
jlColumns.value[4].sortOrder = !sorter ? false : sorter.order
jlData.value.data = jlData.value.data.sort((a, b) => {
if (sorter.order === 'descend') return b.NumberSum - a.NumberSum
return a.NumberSum - b.NumberSum
})
break
}
jlLoading.value = false
}
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,214 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-grid class="mb-10" x-gap="12" :cols="4">
<n-gi>
<n-date-picker
v-model:formatted-value="range"
value-format="yyyy-MM-dd"
type="daterange"
clearable
/>
</n-gi>
<n-gi>
<n-button type="primary" @click="getList">搜索</n-button>
<n-button ml-10 @click="clear">重置</n-button>
</n-gi>
</n-grid>
<div w-full flex items-center>
<Echarts :loading="loading" :option="option" />
<Echarts :loading="loading" :option="option1" />
</div>
<div w-full flex items-center justify-between>
<n-card title="开奖记录" :bordered="false" content-style="padding: 0;">
<n-data-table
:max-height="500"
:loading="loading"
:columns="columns"
:data="data"
:bordered="true"
:virtual-scroll="true"
remote
/>
</n-card>
</div>
</CommonPage>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import api from '../../api.js'
import Echarts from '@/components/Echarts.vue'
import dayjs from 'dayjs'
const loading = ref(false)
const range = ref(null)
const option = ref({
title: {
text: '单期下注(豆子)/赔付(积分) 统计',
left: 'center',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: [],
},
yAxis: {
type: 'value',
},
series: [
{
name: '下注(豆子)',
data: [],
type: 'bar',
},
{
name: '赔付(积分)',
data: [],
type: 'bar',
},
],
dataZoom: [
{
type: 'inside',
},
{
type: 'slider',
},
],
})
const option1 = ref({
title: {
text: '总下注(豆子)/总赔付(积分)',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'right',
},
series: [
{
type: 'pie',
radius: '50%',
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
})
const data = ref([])
const columns = ref([
{
title: '期数',
key: 'Periods',
align: 'center',
},
{
title: '购买号码',
key: 'Name',
align: 'center',
},
{
title: '下注',
key: 'NumberSum',
align: 'center',
},
{
title: '赔付',
key: 'TotalCount',
align: 'center',
},
{
title: '时间',
key: 'Date',
align: 'center',
},
])
onMounted(() => {
getList()
})
const clear = () => {
range.value = null
getList()
}
const getList = async () => {
loading.value = true
const dataObj = {
StartTime: dayjs().format('YYYY-MM-DD'),
EndTime: dayjs().format('YYYY-MM-DD'),
}
if (range.value) {
dataObj.StartTime = range.value[0]
dataObj.EndTime = range.value[1]
}
const res = await api.getStatistics(dataObj)
const newData = res.data.data || []
data.value = newData
option.value.xAxis.data = []
option.value.series[0].data = []
option.value.series[1].data = []
option1.value.series[0].data = []
if (newData.length > 0) {
res.data.data.forEach((item) => {
const a = (
((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) *
100
).toFixed(2)
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
2
)
option.value.xAxis.data.push(`${item.Periods}期-${item.Name}`)
option.value.series[0].name = `下注(豆子): ${a}%`
option.value.series[0].data.push(item.NumberSum * 10)
option.value.series[1].name = `赔付(积分): ${b}%`
option.value.series[1].data.push(item.TotalCount)
})
const a = (((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
2
)
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(2)
option1.value.series[0].data.push({ value: res.data.total * 10, name: `总下注: ${a}%` })
option1.value.series[0].data.push({
value: res.data.totalDices,
name: `总赔付: ${b}%`,
})
}
loading.value = false
}
</script>
<style lang="scss" scoped>
.chart {
width: 50%;
height: 400px;
}
</style>

View File

@@ -0,0 +1,189 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-data-table
:max-height="500"
:loading="loading"
:columns="columns"
:data="data"
:bordered="true"
:virtual-scroll="true"
:pagination="pagination"
remote
/>
<!-- -->
<n-modal v-model:show="showModal">
<n-card
style="width: 800px"
title="宙斯的眷顾"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-data-table
:loading="zsLoading"
:columns="zsColumns"
:data="zsData"
:pagination="zsPagination"
:bordered="false"
/>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import api from '../../api.js'
import { NButton } from 'naive-ui'
import { h, ref, onMounted } from 'vue'
const loading = ref(false)
const columns = ref([
{
title: '期数',
key: 'periods',
align: 'center',
},
{
title: '开奖号码',
key: 'betting_name',
align: 'center',
},
{
title: '开奖数字',
key: 'betting_number',
align: 'center',
},
{
title: '操作人',
key: 'name',
align: 'center',
},
{
title: '开奖时间',
key: 'draw_time',
align: 'center',
},
{
title: '操作时间',
key: 'add_time',
align: 'center',
},
{
title: '操作IP',
key: 'ip',
align: 'center',
},
{
title: '操作',
slot: 'action',
align: 'center',
render: (row) => {
return [
h(
NButton,
{
strong: true,
secondary: true,
onClick: () => {
openModal(row)
},
},
{ default: () => '查看' }
),
]
},
},
])
const data = ref([])
onMounted(() => {
getData()
})
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getData()
},
})
const getData = async () => {
const res = await api.getLog({
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
}
const showModal = ref(false)
const zsLoading = ref(false)
const zsColumns = ref([
{
title: '期数',
key: 'Periods',
align: 'center',
},
{
title: '数字',
key: 'PeriodsNum',
align: 'center',
},
{
title: '总投注(豆子)',
key: 'NumberSum',
align: 'center',
},
{
title: '赔付(积分)',
key: 'TotalCount',
align: 'center',
},
{
title: '选中用户',
key: 'User',
align: 'center',
},
{
title: '电话号码',
key: 'Phone',
align: 'center',
},
])
const zsData = ref([])
const zsPagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
zsPagination.value.page = page
getData()
},
})
const openModal = async (row) => {
showModal.value = true
zsLoading.value = true
const res = await api.getDetail({
periods: row.periods,
draw_time: row.draw_time,
pageNum: zsPagination.value.page,
pageSize: zsPagination.value.pageSize,
})
zsData.value = res.data.data || []
zsPagination.value.itemCount = res.data.total
zsLoading.value = false
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,282 @@
<script setup>
import { h, ref, resolveDirective, withDirectives } from 'vue'
import api from '../api'
import { NTag, NImage, NButton, NDropdown } from 'naive-ui'
import Upload from '@/components/Upload.vue'
import Editor from '@/components/Editor.vue'
import { useRouter } from 'vue-router'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const showModal = ref(false)
const columns = ref([
{
title: '游戏封面',
align: 'center',
slot: 'cover',
render: (row) => {
return h(NImage, {
width: '50',
src: row.cover,
})
},
},
{
title: '游戏名称',
align: 'center',
key: 'name',
},
{
title: '是否开启',
align: 'center',
slot: 'status',
render: (row) => {
return h(
NTag,
{
type: row.status === 1 ? 'success' : 'warning',
},
{
default: () => (row.status === 1 ? '开启' : '禁用'),
}
)
},
},
{
title: '操作',
align: 'center',
slot: 'action',
render: (row) => {
return [
withDirectives(
h(
NButton,
{
type: 'primary',
text: true,
size: 'small',
onClick: () => {
openModal(2, row)
},
},
() => '编辑'
),
[[vPerms, ['/admin/game/edit']]]
),
withDirectives(
h(
'span',
{
class: 'ml-10',
},
{
default: () => [
h(
NDropdown,
{
trigger: 'click',
size: 'small',
options: [
{
key: 0,
label: '实时数据',
},
{
key: 1,
label: '数据统计',
},
{
key: 2,
label: '宙斯统计',
},
],
onSelect: (e) => dropdownSelect(e),
},
{
default: () =>
h(
NButton,
{
class: 'ml-5',
type: 'primary',
text: true,
size: 'small',
},
{
default: () => '操作',
}
),
}
),
],
}
),
[[vPerms, ['待定']]]
),
]
},
},
])
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
})
onMounted(() => {
getList()
})
const getList = async () => {
const res = await api.getGameList({
pageSize: pagination.value.pageSize,
pageNum: pagination.value.page,
})
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
}
const openModal = (type = null, row) => {
if (type) {
model.value = {
...row,
cover: row.cover ? [{ url: row.cover, name: '图片', status: 'finished' }] : [],
}
}
showModal.value = true
}
const formRef = ref(null)
const rules = {
cover: {
required: true,
type: 'array',
message: '请选择游戏封面',
trigger: 'change',
},
name: {
required: true,
message: '请输入游戏名称',
trigger: 'blur',
},
url: {
required: true,
message: '请输入游戏链接',
trigger: 'blur',
},
introduction: {
required: true,
message: '请输入游戏介绍',
trigger: 'blur',
},
}
const model = ref({
name: '',
url: '',
introduction: '',
cover: [],
status: 2,
})
const submit = () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
const data = {
...model.value,
cover: model.value.cover[0].url,
}
const res = await api.addGame(data)
$message.success(res.msg)
clear()
}
})
}
const clear = () => {
model.value = {
name: '',
url: '',
introduction: '',
cover: [],
status: 2,
}
formRef.value?.restoreValidation()
showModal.value = false
getList()
}
const route = useRouter()
const paths = ['/game/game_data', '/game/game_statistics', '/game/game_zs']
const dropdownSelect = (e) => {
route.push(paths[e])
}
</script>
<template>
<CommonPage show-footer :title="$route.title">
<n-button v-perms="['/admin/game/edit']" type="primary" @click="openModal()">添加游戏</n-button>
<n-data-table
class="mt-5"
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<!-- 添加/编辑游戏 -->
<n-modal v-model:show="showModal">
<n-card
style="width: 900px"
title="添加/编辑游戏"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="16" label="游戏封面:" path="cover">
<Upload v-model:list="model.cover" :max="1" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="游戏名称:" path="name">
<n-input v-model:value="model.name" placeholder="请输入游戏名称" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="游戏链接:" path="url">
<n-input v-model:value="model.url" placeholder="请输入游戏链接" />
</n-form-item-gi>
<n-form-item-gi :span="24" label="游戏介绍:" path="introduction">
<Editor v-model:value-html="model.introduction" :height="350" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="是否显示:" path="status">
<n-switch v-model:value="model.status" :unchecked-value="2" :checked-value="1" />
</n-form-item-gi>
<n-form-item-gi :span="12">
<div m-auto p-10>
<n-button type="primary" @click="submit">提交</n-button>
<n-button ml-10 @click="clear">取消</n-button>
</div>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
</n-modal>
</CommonPage>
</template>
<style scoped lang="scss"></style>

View File

@@ -5,6 +5,11 @@ export default {
path: '/game',
component: Layout,
redirect: '/game_data',
meta: {
title: '游戏管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'Gamelist',
@@ -12,6 +17,27 @@ export default {
component: () => import('./data/index.vue'),
meta: {
title: '实时数据',
icon: 'mdi:account-multiple',
order: 10,
},
},
{
name: 'statistics',
path: 'game_statistics',
component: () => import('./statistics/index.vue'),
meta: {
title: '数据统计',
icon: 'mdi:account-multiple',
order: 10,
},
},
{
name: 'zs',
path: 'game_zs',
component: () => import('./zs/index.vue'),
meta: {
title: '宙斯统计',
icon: 'mdi:account-multiple',
order: 10,
},
},

View File

@@ -100,8 +100,8 @@ async function handleLogin() {
loading.value = true
$message.loading('正在验证...')
const res = await api.login({ phone: name, password: password.toString() })
console.log(res.data.token)
$message.success('登录成功')
window.localStorage.setItem('menu', JSON.stringify(res.data.auth))
setToken(res.data.token)
if (isRemember.value) {
lStorage.set('loginInfo', { name, password })
@@ -117,7 +117,6 @@ async function handleLogin() {
router.push('/')
}
} catch (error) {
console.error(error)
$message.removeMessage()
}
loading.value = false

View File

@@ -0,0 +1,6 @@
import { request } from '@/utils'
export default {
getSignConfig: () => request.post('/gift/setting'),
setSignConfig: (data) => request.post('/gift/setting/edit', data),
}

View File

@@ -0,0 +1,65 @@
<script setup>
import api from '../api'
const formRef = ref(null)
const formData = ref({})
const rules = {}
onMounted(() => {
get_config()
})
const get_config = async () => {
const res = await api.getSignConfig()
formData.value = res.data.data[0]
}
const ok = () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
const data = {
Gift: formData.value.gift,
Sign: formData.value.sign,
WeekSign: formData.value.week_sign,
MonthSign: formData.value.month_sign,
}
await api.setSignConfig(data)
$message.success('保存成功')
} catch (e) {
$message.error(e.msg)
}
}
})
}
</script>
<template>
<CommonPage show-footer :title="$route.title">
<n-form ref="formRef" :model="formData" :rules="rules">
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="24" label="注册赠送" path="gift">
<n-input-number v-model:value="formData.gift" :step="100" />
</n-form-item-gi>
<n-form-item-gi :span="24" label="每日赠送" path="sign">
<n-input-number v-model:value="formData.sign" :step="100" />
</n-form-item-gi>
<n-form-item-gi :span="24" label="每周赠送" path="week_sign">
<n-input-number v-model:value="formData.week_sign" :step="100" />
</n-form-item-gi>
<n-form-item-gi :span="24" label="每月赠送" path="month_sign">
<n-input-number v-model:value="formData.month_sign" :step="100" />
</n-form-item-gi>
<n-form-item-gi :span="12">
<n-button v-perms="['/admin/gift/setting/edit']" type="primary" @click="ok">
保存
</n-button>
</n-form-item-gi>
</n-grid>
</n-form>
</CommonPage>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,11 +1,101 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-row gutter="12">
<n-col :span="24">
<div flex>
<n-card w-500>
<n-statistic label="订单流水(元)" tabular-nums>
<n-number-animation :from="0" :to="cardData.total" :precision="2" />
</n-statistic>
</n-card>
<!-- <n-card ml-10 w-500>
<n-statistic label="订单佣金(元)" tabular-nums>
<n-number-animation :from="0" :to="Number(cardData.service)" :precision="2" />
</n-statistic>
</n-card> -->
<n-card ml-10 w-500>
<n-statistic label="订单数量" tabular-nums>
<n-number-animation :from="0" :to="cardData.count" />
</n-statistic>
</n-card>
</div>
</n-col>
<n-col :span="24" mt-10>
<div>
<span>订单状态</span>
<n-radio-group v-model:value="queryData.status">
<n-radio-button
v-for="song in songs"
:key="song.value"
:value="song.value"
:label="song.label"
/>
</n-radio-group>
</div>
</n-col>
<n-col :span="24" mt-10>
<div>
<span>支付方式</span>
<n-radio-group v-model:value="queryData.pay_type">
<n-radio-button
v-for="song in [
{
label: '微信',
value: 1,
},
{
label: '积分',
value: 2,
},
]"
:key="song.value"
:value="song.value"
:label="song.label"
/>
</n-radio-group>
</div>
</n-col>
<n-col :span="24">
<div mt-10 flex items-center>
<div w-100>关键字搜索:</div>
<n-input-group>
<n-select
v-model:value="queryData.selectKey"
:style="{ width: '15%' }"
:options="selectOptions"
placeholder="请选择"
/>
<n-input v-model:value="queryData.word" :style="{ width: '20%' }" />
</n-input-group>
</div>
</n-col>
<n-col :span="10">
<div mt-10 flex items-center>
<span w-100>订单时间:</span>
<n-date-picker
v-model:formatted-value="queryData.time"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
clearable
/>
</div>
</n-col>
<n-col :span="24">
<div mt-10>
<n-button type="primary" @click="getList">搜索</n-button>
<n-button ml-10 @click="clear">重置</n-button>
</div>
</n-col>
</n-row>
<n-data-table
class="mt-5"
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
:scroll-x="1800"
remote
/>
</CommonPage>
</template>
@@ -15,27 +105,155 @@ import api from './api'
const loading = ref(false)
const queryData = ref({
status: '',
time: null,
word: '',
selectKey: null,
pay_type: null,
})
const cardData = ref({
total: 0,
service: 0,
count: 0,
})
const songs = ref([
{
value: 1,
label: '待付款',
},
{
value: 2,
label: '待核销',
},
{
value: 3,
label: '已核销',
},
{
value: 4,
label: '已过期',
},
{
value: 5,
label: '已销售',
},
])
const selectOptions = ref([
{
value: 0,
label: '商品名称',
},
{
value: 1,
label: '用户昵称',
},
{
value: 2,
label: '手机号',
},
{
value: 3,
label: '订单号',
},
{
value: 4,
label: '商家名称',
},
])
const columns = ref([
{
title: '订单号',
align: 'center',
key: 'oid',
width: 200,
fixed: 'left',
},
{
title: '用户',
align: 'center',
key: 'user_name',
slot: 'user',
render: (row) => {
return [
h(
'div',
{},
{
default: () => row.User.nickName,
}
),
]
},
},
{
title: '用户电话',
align: 'center',
slot: 'phone',
render: (row) => {
return [
h(
'div',
{},
{
default: () => row.User.phone,
}
),
]
},
},
{
title: '商品名称',
align: 'center',
key: 'goods_name',
slot: 'goods_name',
render: (row) => {
const el = []
row.OrderGoods.forEach((item) => {
el.push(
h(
'div',
{},
{
default: () =>
`${item.Goods.name}|${item.pay_price}元或${item.pay_integral}积分|X${item.number}`,
}
)
)
})
return el
},
},
{
title: '商品价格',
title: '商品数量',
align: 'center',
key: 'number',
key: 'count',
},
{
title: '订单总价',
align: 'center',
slot: 'number',
render: (row) => h('span', row.pay_type === 1 ? `${row.price}元` : `${row.exchange}积分`),
},
{
title: '支付方式',
align: 'center',
slot: 'pay_type',
render: (row) => h('span', row.pay_type === 1 ? '微信' : '积分'),
},
{
title: '商家名称',
align: 'center',
slot: 'store_name',
render: (row) => h('span', row.Store.name),
},
// {
// title: '订单佣金(元)',
// align: 'center',
// key: 'commission_number',
// },
{
title: '订单状态',
align: 'center',
@@ -45,20 +263,30 @@ const columns = ref([
case 0:
return h('span', '待付款')
case 1:
return h('span', '待使用')
return h('span', '待核销')
case 2:
return h('span', '已完成')
return h('span', '已核销')
case 3:
return h('span', '已过期')
}
},
},
{
title: '下单时间',
align: 'center',
key: 'add_time',
},
{
title: '核销时间',
align: 'center',
key: 'cancel_time',
},
{
title: '操作',
align: 'center',
slot: 'action',
render(row) {
console.log(row)
render() {
// console.log(row)
},
},
])
@@ -66,9 +294,9 @@ const columns = ref([
const data = ref([])
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
total: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -87,17 +315,53 @@ onMounted(() => {
const getList = async () => {
loading.value = true
try {
const query_data = {
Status: queryData.value.status,
PayType: queryData.value.pay_type || '',
StartTime: queryData.value.time === null ? '' : queryData.value.time[0] || '',
EndTime: queryData.value.time === null ? '' : queryData.value.time[1] || '',
}
switch (queryData.value.selectKey) {
case 0:
query_data['GoodsName'] = queryData.value.word
break
case 1:
query_data['UserName'] = queryData.value.word
break
case 2:
query_data['Phone'] = queryData.value.word
break
case 3:
query_data['Oid'] = queryData.value.word
break
case 4:
query_data['StoreName'] = queryData.value.word
}
const res = await api.getOrder({
pageNum: pagination.value.current,
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
...query_data,
})
console.log(res)
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
cardData.value.total = res.data.number
cardData.value.service = res.data.commission
cardData.value.count = res.data.total
} catch (error) {
$message.error(error.msg)
}
loading.value = false
}
const clear = () => {
queryData.value = {
status: '',
time: null,
word: '',
selectKey: null,
}
getList()
}
</script>
<style lang="scss" scoped></style>

View File

@@ -1,41 +1,200 @@
<template>
<CommonPage show-footer :title="$route.title">
<!-- {{ queryData }} -->
<n-row gutter="12">
<n-col :span="24">
<div flex>
<n-card w-500>
<n-statistic label="订单流水(积分)" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.total" />
</n-statistic>
</n-card>
<n-card ml-10 w-500>
<n-statistic label="订单佣金(积分)" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.service" />
</n-statistic>
</n-card>
<n-card ml-10 w-500>
<n-statistic label="订单数量" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.count" />
</n-statistic>
</n-card>
</div>
</n-col>
<n-col :span="24">
<div mt-10>
<span>订单状态</span>
<n-radio-group v-model:value="queryData.status">
<n-radio-button
v-for="song in songs"
:key="song.value"
:value="song.value"
:label="song.label"
/>
</n-radio-group>
</div>
</n-col>
<n-col :span="24">
<div mt-10 flex items-center>
<div w-100>订单搜索</div>
<n-input-group>
<n-select
v-model:value="queryData.selectKey"
:style="{ width: '15%' }"
:options="selectOptions"
placeholder="请选择"
/>
<n-input v-model:value="queryData.word" :style="{ width: '20%' }" />
</n-input-group>
</div>
</n-col>
<n-col :span="10">
<div mt-10 flex items-center>
<span w-100>订单时间</span>
<n-date-picker
v-model:formatted-value="queryData.time"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
clearable
/>
</div>
</n-col>
<n-col :span="24">
<div mt-10>
<n-button type="primary" @click="getList">搜索</n-button>
<n-button ml-10 @click="clear">重置</n-button>
</div>
</n-col>
</n-row>
<n-data-table
class="mt-5"
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
</CommonPage>
</template>
<script setup>
import api from './api'
import { NEllipsis } from 'naive-ui'
const loading = ref(false)
const cardData = ref({
total: 0,
service: 0,
count: 0,
})
const queryData = ref({
status: '',
time: null,
word: '',
selectKey: null,
})
const songs = ref([
{
value: 1,
label: '待付款',
},
{
value: 2,
label: '待核销',
},
{
value: 3,
label: '已核销',
},
{
value: 4,
label: '已过期',
},
{
value: 5,
label: '已销售',
},
])
const selectOptions = ref([
{
value: 0,
label: '商品名称',
},
{
value: 1,
label: '用户昵称',
},
{
value: 2,
label: '手机号',
},
{
value: 3,
label: '订单号',
},
{
value: 4,
label: '商家名称',
},
])
const columns = ref([
{
title: '订单号',
align: 'center',
key: 'oid',
},
{
title: '订单归属商户',
align: 'center',
key: 'store_name',
},
{
title: '用户',
align: 'center',
key: 'user_name',
},
{
title: '商品名称',
title: '用户电话',
align: 'center',
key: 'goods_name',
key: 'phone',
},
{
title: '商品价格',
title: '商品名称',
align: 'center',
slot: 'goods_name',
render: (row) => {
return h(
NEllipsis,
{
style: 'max-width: 240px',
},
{
default: () => row.goods_name,
}
)
},
},
{
title: '商品数量',
align: 'center',
key: 'count',
},
{
title: '订单总价',
align: 'center',
key: 'number',
},
{
title: '订单佣金',
align: 'center',
key: 'commission',
},
{
title: '订单状态',
align: 'center',
@@ -45,30 +204,40 @@ const columns = ref([
case 0:
return h('span', '待付款')
case 1:
return h('span', '待使用')
return h('span', '待核销')
case 2:
return h('span', '已完成')
return h('span', '已核销')
case 3:
return h('span', '已过期')
}
},
},
{
title: '操作',
title: '下单时间',
align: 'center',
slot: 'action',
render(row) {
console.log(row)
},
key: 'add_time',
},
{
title: '核销时间',
align: 'center',
key: 'cancel_time',
},
// {
// title: '操作',
// align: 'center',
// slot: 'action',
// render(row) {
// console.log(row)
// },
// },
])
const data = ref([])
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
total: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -87,17 +256,53 @@ onMounted(() => {
const getList = async () => {
loading.value = true
try {
const query_data = {
Status: queryData.value.status || '',
StartTime: queryData.value.time === null ? '' : queryData.value.time[0] || '',
EndTime: queryData.value.time === null ? '' : queryData.value.time[1] || '',
}
switch (queryData.value.selectKey) {
case 0:
query_data['GoodsName'] = queryData.value.word
break
case 1:
query_data['UserName'] = queryData.value.word
break
case 2:
query_data['Phone'] = queryData.value.word
break
case 3:
query_data['Oid'] = queryData.value.word
break
case 4:
query_data['StoreName'] = queryData.value.word
break
}
const res = await api.getPoint({
pageNum: pagination.value.current,
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
...query_data,
})
console.log(res)
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
cardData.value.total = res.data.number
cardData.value.service = res.data.commission
cardData.value.count = res.data.total
} catch (error) {
$message.error(error.msg)
}
loading.value = false
}
const clear = () => {
queryData.value = {
status: '',
time: null,
word: '',
selectKey: null,
}
getList()
}
</script>
<style lang="scss" scoped></style>

View File

@@ -5,6 +5,11 @@ export default {
path: '/order',
component: Layout,
redirect: '/order_list',
meta: {
title: '订单管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'Orderlist',

View File

@@ -0,0 +1,805 @@
{
"name": "Ant Design Icons",
"total": 789,
"version": "4.3.1",
"license": {
"title": "MIT",
"spdx": "MIT"
},
"samples": ["pushpin-filled", "pie-chart-outlined", "shopping-twotone"],
"height": 16,
"category": "General",
"palette": false,
"id": "ant-design",
"icons": [
"account-book-filled",
"account-book-outlined",
"account-book-twotone",
"aim-outlined",
"alert-filled",
"alert-outlined",
"alert-twotone",
"alibaba-outlined",
"align-center-outlined",
"align-left-outlined",
"align-right-outlined",
"alipay-circle-filled",
"alipay-circle-outlined",
"alipay-outlined",
"alipay-square-filled",
"aliwangwang-filled",
"aliwangwang-outlined",
"aliyun-outlined",
"amazon-circle-filled",
"amazon-outlined",
"amazon-square-filled",
"android-filled",
"android-outlined",
"ant-cloud-outlined",
"ant-design-outlined",
"apartment-outlined",
"api-filled",
"api-outlined",
"api-twotone",
"apple-filled",
"apple-outlined",
"appstore-add-outlined",
"appstore-filled",
"appstore-outlined",
"appstore-twotone",
"area-chart-outlined",
"arrow-down-outlined",
"arrow-left-outlined",
"arrow-right-outlined",
"arrow-up-outlined",
"arrows-alt-outlined",
"audio-filled",
"audio-muted-outlined",
"audio-outlined",
"audio-twotone",
"audit-outlined",
"backward-filled",
"backward-outlined",
"bank-filled",
"bank-outlined",
"bank-twotone",
"bar-chart-outlined",
"barcode-outlined",
"bars-outlined",
"behance-circle-filled",
"behance-outlined",
"behance-square-filled",
"behance-square-outlined",
"bell-filled",
"bell-outlined",
"bell-twotone",
"bg-colors-outlined",
"block-outlined",
"bold-outlined",
"book-filled",
"book-outlined",
"book-twotone",
"border-bottom-outlined",
"border-horizontal-outlined",
"border-inner-outlined",
"border-left-outlined",
"border-outer-outlined",
"border-outlined",
"border-right-outlined",
"border-top-outlined",
"border-verticle-outlined",
"borderless-table-outlined",
"box-plot-filled",
"box-plot-outlined",
"box-plot-twotone",
"branches-outlined",
"bug-filled",
"bug-outlined",
"bug-twotone",
"build-filled",
"build-outlined",
"build-twotone",
"bulb-filled",
"bulb-outlined",
"bulb-twotone",
"calculator-filled",
"calculator-outlined",
"calculator-twotone",
"calendar-filled",
"calendar-outlined",
"calendar-twotone",
"camera-filled",
"camera-outlined",
"camera-twotone",
"car-filled",
"car-outlined",
"car-twotone",
"caret-down-filled",
"caret-down-outlined",
"caret-left-filled",
"caret-left-outlined",
"caret-right-filled",
"caret-right-outlined",
"caret-up-filled",
"caret-up-outlined",
"carry-out-filled",
"carry-out-outlined",
"carry-out-twotone",
"check-circle-filled",
"check-circle-outlined",
"check-circle-twotone",
"check-outlined",
"check-square-filled",
"check-square-outlined",
"check-square-twotone",
"chrome-filled",
"chrome-outlined",
"ci-circle-filled",
"ci-circle-outlined",
"ci-circle-twotone",
"ci-outlined",
"ci-twotone",
"clear-outlined",
"clock-circle-filled",
"clock-circle-outlined",
"clock-circle-twotone",
"close-circle-filled",
"close-circle-outlined",
"close-circle-twotone",
"close-outlined",
"close-square-filled",
"close-square-outlined",
"close-square-twotone",
"cloud-download-outlined",
"cloud-filled",
"cloud-outlined",
"cloud-server-outlined",
"cloud-sync-outlined",
"cloud-twotone",
"cloud-upload-outlined",
"cluster-outlined",
"code-filled",
"code-outlined",
"code-sandbox-circle-filled",
"code-sandbox-outlined",
"code-sandbox-square-filled",
"code-twotone",
"codepen-circle-filled",
"codepen-circle-outlined",
"codepen-outlined",
"codepen-square-filled",
"coffee-outlined",
"column-height-outlined",
"column-width-outlined",
"comment-outlined",
"compass-filled",
"compass-outlined",
"compass-twotone",
"compress-outlined",
"console-sql-outlined",
"contacts-filled",
"contacts-outlined",
"contacts-twotone",
"container-filled",
"container-outlined",
"container-twotone",
"control-filled",
"control-outlined",
"control-twotone",
"copy-filled",
"copy-outlined",
"copy-twotone",
"copyright-circle-filled",
"copyright-circle-outlined",
"copyright-circle-twotone",
"copyright-outlined",
"copyright-twotone",
"credit-card-filled",
"credit-card-outlined",
"credit-card-twotone",
"crown-filled",
"crown-outlined",
"crown-twotone",
"customer-service-filled",
"customer-service-outlined",
"customer-service-twotone",
"dash-outlined",
"dashboard-filled",
"dashboard-outlined",
"dashboard-twotone",
"database-filled",
"database-outlined",
"database-twotone",
"delete-column-outlined",
"delete-filled",
"delete-outlined",
"delete-row-outlined",
"delete-twotone",
"delivered-procedure-outlined",
"deployment-unit-outlined",
"desktop-outlined",
"diff-filled",
"diff-outlined",
"diff-twotone",
"dingding-outlined",
"dingtalk-circle-filled",
"dingtalk-outlined",
"dingtalk-square-filled",
"disconnect-outlined",
"dislike-filled",
"dislike-outlined",
"dislike-twotone",
"dollar-circle-filled",
"dollar-circle-outlined",
"dollar-circle-twotone",
"dollar-outlined",
"dollar-twotone",
"dot-chart-outlined",
"double-left-outlined",
"double-right-outlined",
"down-circle-filled",
"down-circle-outlined",
"down-circle-twotone",
"down-outlined",
"down-square-filled",
"down-square-outlined",
"down-square-twotone",
"download-outlined",
"drag-outlined",
"dribbble-circle-filled",
"dribbble-outlined",
"dribbble-square-filled",
"dribbble-square-outlined",
"dropbox-circle-filled",
"dropbox-outlined",
"dropbox-square-filled",
"edit-filled",
"edit-outlined",
"edit-twotone",
"ellipsis-outlined",
"enter-outlined",
"environment-filled",
"environment-outlined",
"environment-twotone",
"euro-circle-filled",
"euro-circle-outlined",
"euro-circle-twotone",
"euro-outlined",
"euro-twotone",
"exception-outlined",
"exclamation-circle-filled",
"exclamation-circle-outlined",
"exclamation-circle-twotone",
"exclamation-outlined",
"expand-alt-outlined",
"expand-outlined",
"experiment-filled",
"experiment-outlined",
"experiment-twotone",
"export-outlined",
"eye-filled",
"eye-invisible-filled",
"eye-invisible-outlined",
"eye-invisible-twotone",
"eye-outlined",
"eye-twotone",
"facebook-filled",
"facebook-outlined",
"fall-outlined",
"fast-backward-filled",
"fast-backward-outlined",
"fast-forward-filled",
"fast-forward-outlined",
"field-binary-outlined",
"field-number-outlined",
"field-string-outlined",
"field-time-outlined",
"file-add-filled",
"file-add-outlined",
"file-add-twotone",
"file-done-outlined",
"file-excel-filled",
"file-excel-outlined",
"file-excel-twotone",
"file-exclamation-filled",
"file-exclamation-outlined",
"file-exclamation-twotone",
"file-filled",
"file-gif-outlined",
"file-image-filled",
"file-image-outlined",
"file-image-twotone",
"file-jpg-outlined",
"file-markdown-filled",
"file-markdown-outlined",
"file-markdown-twotone",
"file-outlined",
"file-pdf-filled",
"file-pdf-outlined",
"file-pdf-twotone",
"file-ppt-filled",
"file-ppt-outlined",
"file-ppt-twotone",
"file-protect-outlined",
"file-search-outlined",
"file-sync-outlined",
"file-text-filled",
"file-text-outlined",
"file-text-twotone",
"file-twotone",
"file-unknown-filled",
"file-unknown-outlined",
"file-unknown-twotone",
"file-word-filled",
"file-word-outlined",
"file-word-twotone",
"file-zip-filled",
"file-zip-outlined",
"file-zip-twotone",
"filter-filled",
"filter-outlined",
"filter-twotone",
"fire-filled",
"fire-outlined",
"fire-twotone",
"flag-filled",
"flag-outlined",
"flag-twotone",
"folder-add-filled",
"folder-add-outlined",
"folder-add-twotone",
"folder-filled",
"folder-open-filled",
"folder-open-outlined",
"folder-open-twotone",
"folder-outlined",
"folder-twotone",
"folder-view-outlined",
"font-colors-outlined",
"font-size-outlined",
"fork-outlined",
"form-outlined",
"format-painter-filled",
"format-painter-outlined",
"forward-filled",
"forward-outlined",
"frown-filled",
"frown-outlined",
"frown-twotone",
"fullscreen-exit-outlined",
"fullscreen-outlined",
"function-outlined",
"fund-filled",
"fund-outlined",
"fund-projection-screen-outlined",
"fund-twotone",
"fund-view-outlined",
"funnel-plot-filled",
"funnel-plot-outlined",
"funnel-plot-twotone",
"gateway-outlined",
"gif-outlined",
"gift-filled",
"gift-outlined",
"gift-twotone",
"github-filled",
"github-outlined",
"gitlab-filled",
"gitlab-outlined",
"global-outlined",
"gold-filled",
"gold-outlined",
"gold-twotone",
"golden-filled",
"google-circle-filled",
"google-outlined",
"google-plus-circle-filled",
"google-plus-outlined",
"google-plus-square-filled",
"google-square-filled",
"group-outlined",
"hdd-filled",
"hdd-outlined",
"hdd-twotone",
"heart-filled",
"heart-outlined",
"heart-twotone",
"heat-map-outlined",
"highlight-filled",
"highlight-outlined",
"highlight-twotone",
"history-outlined",
"holder-outlined",
"home-filled",
"home-outlined",
"home-twotone",
"hourglass-filled",
"hourglass-outlined",
"hourglass-twotone",
"html5-filled",
"html5-outlined",
"html5-twotone",
"idcard-filled",
"idcard-outlined",
"idcard-twotone",
"ie-circle-filled",
"ie-outlined",
"ie-square-filled",
"import-outlined",
"inbox-outlined",
"info-circle-filled",
"info-circle-outlined",
"info-circle-twotone",
"info-outlined",
"insert-row-above-outlined",
"insert-row-below-outlined",
"insert-row-left-outlined",
"insert-row-right-outlined",
"instagram-filled",
"instagram-outlined",
"insurance-filled",
"insurance-outlined",
"insurance-twotone",
"interaction-filled",
"interaction-outlined",
"interaction-twotone",
"issues-close-outlined",
"italic-outlined",
"key-outlined",
"laptop-outlined",
"layout-filled",
"layout-outlined",
"layout-twotone",
"left-circle-filled",
"left-circle-outlined",
"left-circle-twotone",
"left-outlined",
"left-square-filled",
"left-square-outlined",
"left-square-twotone",
"like-filled",
"like-outlined",
"like-twotone",
"line-chart-outlined",
"line-height-outlined",
"line-outlined",
"link-outlined",
"linkedin-filled",
"linkedin-outlined",
"loading-3-quarters-outlined",
"loading-outlined",
"lock-filled",
"lock-outlined",
"lock-twotone",
"login-outlined",
"logout-outlined",
"mac-command-filled",
"mac-command-outlined",
"mail-filled",
"mail-outlined",
"mail-twotone",
"man-outlined",
"medicine-box-filled",
"medicine-box-outlined",
"medicine-box-twotone",
"medium-circle-filled",
"medium-outlined",
"medium-square-filled",
"medium-workmark-outlined",
"meh-filled",
"meh-outlined",
"meh-twotone",
"menu-fold-outlined",
"menu-outlined",
"menu-unfold-outlined",
"merge-cells-outlined",
"message-filled",
"message-outlined",
"message-twotone",
"minus-circle-filled",
"minus-circle-outlined",
"minus-circle-twotone",
"minus-outlined",
"minus-square-filled",
"minus-square-outlined",
"minus-square-twotone",
"mobile-filled",
"mobile-outlined",
"mobile-twotone",
"money-collect-filled",
"money-collect-outlined",
"money-collect-twotone",
"monitor-outlined",
"more-outlined",
"node-collapse-outlined",
"node-expand-outlined",
"node-index-outlined",
"notification-filled",
"notification-outlined",
"notification-twotone",
"number-outlined",
"one-to-one-outlined",
"ordered-list-outlined",
"paper-clip-outlined",
"partition-outlined",
"pause-circle-filled",
"pause-circle-outlined",
"pause-circle-twotone",
"pause-outlined",
"pay-circle-filled",
"pay-circle-outlined",
"percentage-outlined",
"phone-filled",
"phone-outlined",
"phone-twotone",
"pic-center-outlined",
"pic-left-outlined",
"pic-right-outlined",
"picture-filled",
"picture-outlined",
"picture-twotone",
"pie-chart-filled",
"pie-chart-outlined",
"pie-chart-twotone",
"play-circle-filled",
"play-circle-outlined",
"play-circle-twotone",
"play-square-filled",
"play-square-outlined",
"play-square-twotone",
"plus-circle-filled",
"plus-circle-outlined",
"plus-circle-twotone",
"plus-outlined",
"plus-square-filled",
"plus-square-outlined",
"plus-square-twotone",
"pound-circle-filled",
"pound-circle-outlined",
"pound-circle-twotone",
"pound-outlined",
"poweroff-outlined",
"printer-filled",
"printer-outlined",
"printer-twotone",
"profile-filled",
"profile-outlined",
"profile-twotone",
"project-filled",
"project-outlined",
"project-twotone",
"property-safety-filled",
"property-safety-outlined",
"property-safety-twotone",
"pull-request-outlined",
"pushpin-filled",
"pushpin-outlined",
"pushpin-twotone",
"qq-circle-filled",
"qq-outlined",
"qq-square-filled",
"qrcode-outlined",
"question-circle-filled",
"question-circle-outlined",
"question-circle-twotone",
"question-outlined",
"radar-chart-outlined",
"radius-bottomleft-outlined",
"radius-bottomright-outlined",
"radius-setting-outlined",
"radius-upleft-outlined",
"radius-upright-outlined",
"read-filled",
"read-outlined",
"reconciliation-filled",
"reconciliation-outlined",
"reconciliation-twotone",
"red-envelope-filled",
"red-envelope-outlined",
"red-envelope-twotone",
"reddit-circle-filled",
"reddit-outlined",
"reddit-square-filled",
"redo-outlined",
"reload-outlined",
"rest-filled",
"rest-outlined",
"rest-twotone",
"retweet-outlined",
"right-circle-filled",
"right-circle-outlined",
"right-circle-twotone",
"right-outlined",
"right-square-filled",
"right-square-outlined",
"right-square-twotone",
"rise-outlined",
"robot-filled",
"robot-outlined",
"rocket-filled",
"rocket-outlined",
"rocket-twotone",
"rollback-outlined",
"rotate-left-outlined",
"rotate-right-outlined",
"safety-certificate-filled",
"safety-certificate-outlined",
"safety-certificate-twotone",
"safety-outlined",
"save-filled",
"save-outlined",
"save-twotone",
"scan-outlined",
"schedule-filled",
"schedule-outlined",
"schedule-twotone",
"scissor-outlined",
"search-outlined",
"security-scan-filled",
"security-scan-outlined",
"security-scan-twotone",
"select-outlined",
"send-outlined",
"setting-filled",
"setting-outlined",
"setting-twotone",
"shake-outlined",
"share-alt-outlined",
"shop-filled",
"shop-outlined",
"shop-twotone",
"shopping-cart-outlined",
"shopping-filled",
"shopping-outlined",
"shopping-twotone",
"shrink-outlined",
"signal-filled",
"sisternode-outlined",
"sketch-circle-filled",
"sketch-outlined",
"sketch-square-filled",
"skin-filled",
"skin-outlined",
"skin-twotone",
"skype-filled",
"skype-outlined",
"slack-circle-filled",
"slack-outlined",
"slack-square-filled",
"slack-square-outlined",
"sliders-filled",
"sliders-outlined",
"sliders-twotone",
"small-dash-outlined",
"smile-filled",
"smile-outlined",
"smile-twotone",
"snippets-filled",
"snippets-outlined",
"snippets-twotone",
"solution-outlined",
"sort-ascending-outlined",
"sort-descending-outlined",
"sound-filled",
"sound-outlined",
"sound-twotone",
"split-cells-outlined",
"star-filled",
"star-outlined",
"star-twotone",
"step-backward-filled",
"step-backward-outlined",
"step-forward-filled",
"step-forward-outlined",
"stock-outlined",
"stop-filled",
"stop-outlined",
"stop-twotone",
"strikethrough-outlined",
"subnode-outlined",
"swap-left-outlined",
"swap-outlined",
"swap-right-outlined",
"switcher-filled",
"switcher-outlined",
"switcher-twotone",
"sync-outlined",
"table-outlined",
"tablet-filled",
"tablet-outlined",
"tablet-twotone",
"tag-filled",
"tag-outlined",
"tag-twotone",
"tags-filled",
"tags-outlined",
"tags-twotone",
"taobao-circle-filled",
"taobao-circle-outlined",
"taobao-outlined",
"taobao-square-filled",
"team-outlined",
"thunderbolt-filled",
"thunderbolt-outlined",
"thunderbolt-twotone",
"to-top-outlined",
"tool-filled",
"tool-outlined",
"tool-twotone",
"trademark-circle-filled",
"trademark-circle-outlined",
"trademark-circle-twotone",
"trademark-outlined",
"transaction-outlined",
"translation-outlined",
"trophy-filled",
"trophy-outlined",
"trophy-twotone",
"twitter-circle-filled",
"twitter-outlined",
"twitter-square-filled",
"underline-outlined",
"undo-outlined",
"ungroup-outlined",
"unlock-filled",
"unlock-outlined",
"unlock-twotone",
"unordered-list-outlined",
"up-circle-filled",
"up-circle-outlined",
"up-circle-twotone",
"up-outlined",
"up-square-filled",
"up-square-outlined",
"up-square-twotone",
"upload-outlined",
"usb-filled",
"usb-outlined",
"usb-twotone",
"user-add-outlined",
"user-delete-outlined",
"user-outlined",
"user-switch-outlined",
"usergroup-add-outlined",
"usergroup-delete-outlined",
"verified-outlined",
"vertical-align-bottom-outlined",
"vertical-align-middle-outlined",
"vertical-align-top-outlined",
"vertical-left-outlined",
"vertical-right-outlined",
"video-camera-add-outlined",
"video-camera-filled",
"video-camera-outlined",
"video-camera-twotone",
"wallet-filled",
"wallet-outlined",
"wallet-twotone",
"warning-filled",
"warning-outlined",
"warning-twotone",
"wechat-filled",
"wechat-outlined",
"weibo-circle-filled",
"weibo-circle-outlined",
"weibo-outlined",
"weibo-square-filled",
"weibo-square-outlined",
"whats-app-outlined",
"wifi-outlined",
"windows-filled",
"windows-outlined",
"woman-outlined",
"yahoo-filled",
"yahoo-outlined",
"youtube-filled",
"youtube-outlined",
"yuque-filled",
"yuque-outlined",
"zhihu-circle-filled",
"zhihu-outlined",
"zhihu-square-filled",
"zoom-in-outlined",
"zoom-out-outlined"
]
}

View File

@@ -0,0 +1,431 @@
<template>
<CommonPage show-footer :title="'(非技术人员勿动)'">
<n-button v-perms="['/admin/auth/menu/set']" type="primary" @click="openModal(1)">
添加菜单
</n-button>
<n-data-table
class="mt-5"
:loading="loading"
:columns="columns"
:data="data"
:pagination="false"
:bordered="false"
remote
:row-key="rowKey"
children-key="subMenu"
/>
<!-- 添加菜单 -->
<n-modal v-model:show="showModal">
<n-card
style="width: 500px"
title="添加/编辑菜单"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<!-- {{ model }}-->
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="16" label="父级分类:" path="pid">
<n-cascader
v-model:value="model.pid"
placeholder="请选择菜单目录"
expand-trigger="click"
:options="[
{
ID: 0,
name: '顶层菜单',
pid: 0,
},
...data,
]"
check-strategy="all"
show-path
label-field="name"
value-field="ID"
children-field="subMenu"
separator="->"
/>
</n-form-item-gi>
<n-form-item-gi :span="16" label="权限类型:" path="type">
<n-select
v-model:value="model.type"
placeholder="请选择菜单类型"
:options="[
{
label: '目录',
value: 1,
},
{
label: '菜单',
value: 2,
},
{
label: '按钮',
value: 3,
},
]"
/>
</n-form-item-gi>
<n-form-item-gi v-if="model.type !== 3" :span="24" label="菜单图标:" path="icon">
<div>
<n-input-group flex items-center>
<TheIcon :icon="model.icon" :size="30" />
<n-input
v-model:value="model.icon"
placeholder="请选择菜单图标"
:style="{ width: '100%' }"
/>
<n-button type="primary" ghost @click="iconModal = true">+</n-button>
</n-input-group>
</div>
</n-form-item-gi>
<n-form-item-gi v-if="model.type !== 3" :span="10" label="是否开启:" path="status">
<n-switch v-model:value="model.status" :checked-value="1" :unchecked-value="2" />
</n-form-item-gi>
<n-form-item-gi v-if="model.type !== 3" :span="10" label="是否显示:" path="isShow">
<n-switch v-model:value="model.is_show" :checked-value="1" :unchecked-value="2" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="菜单名称:" path="name">
<n-input v-model:value="model.name" placeholder="请填写菜单名称" />
</n-form-item-gi>
<n-form-item-gi
v-if="model.type === 2 || model.type === 1"
:span="16"
label="菜单路径:"
path="route"
>
<n-input v-model:value="model.route" placeholder="请填写菜单路径" />
</n-form-item-gi>
<n-form-item-gi
v-if="model.type === 3 || model.type === 2"
:span="16"
label="权限标识:"
path="api_route"
>
<n-input v-model:value="model.api_route" placeholder="请填写权限标识" />
</n-form-item-gi>
<n-form-item-gi v-if="model.type === 2" :span="16" label="组件路径:" path="components">
<n-input v-model:value="model.components" placeholder="请填写组件路径" />
</n-form-item-gi>
<n-form-item-gi v-if="model.type === 2" :span="16" label="菜单参数:" path="params">
<n-input v-model:value="model.params" placeholder="请填写菜单参数" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="菜单排序:" path="sort">
<n-input-number v-model:value="model.sort" :min="0" placeholder="越低越靠前" />
</n-form-item-gi>
<n-form-item-gi :span="18">
<div m-auto>
<n-button type="primary" @click="handleValidateClick">保存</n-button>
<n-button ml-10 @click="clear">取消</n-button>
</div>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
</n-modal>
<!-- 图标 -->
<n-modal v-model:show="iconModal">
<n-card
style="width: 600px"
title="选择菜单图标"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<div h-500 w-full flex flex-wrap overflow-auto>
<div
v-for="(item, index) in antIconList.icons"
:key="index"
@click="clickIcon(`ant-design:${item}`)"
>
<TheIcon :icon="`ant-design:${item}`" :size="40" />
</div>
</div>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import { h, withDirectives, resolveDirective } from 'vue'
import api from '../api'
import TheIcon from '@/components/icon/TheIcon.vue'
import { NTag, NButton } from 'naive-ui'
import antIconList from './ant-design-meta.json'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const showModal = ref(false)
const iconModal = ref(false)
const rowKey = (row) => {
return row.subMenu || []
}
const columns = ref([
{
title: '菜单名称',
key: 'name',
align: 'center',
},
{
title: '菜单图标',
slot: 'icon',
align: 'center',
render: (row) => {
return [
h(TheIcon, {
icon: row.icon,
size: 20,
}),
]
},
},
{
title: '类型',
slot: 'type',
align: 'center',
render: (row) => {
return [
h(
'span',
{},
{
default: () => (row.type === 1 ? '目录' : row.type === 2 ? '菜单' : '按钮'),
}
),
]
},
},
{
title: '路径',
key: 'route',
align: 'center',
},
{
title: '权限标识',
key: 'api_route',
align: 'center',
},
{
title: '路由参数',
key: 'params',
align: 'center',
},
{
title: '组件路径',
key: 'components',
align: 'center',
},
{
title: '排序',
key: 'sort',
align: 'center',
},
{
title: '是否启用',
solt: 'status',
align: 'center',
render: (row) => {
return [
h(
NTag,
{
type: row.status === 1 ? 'success' : 'warning',
},
{
default: () => (row.status === 1 ? '启用' : '禁用'),
}
),
]
},
},
{
title: '是否显示',
solt: 'status',
align: 'center',
render: (row) => {
return [
h(
NTag,
{
type: row.is_show === 1 ? 'success' : 'warning',
},
{
default: () => (row.is_show === 1 ? '显示' : '隐藏'),
}
),
]
},
},
{
title: '操作',
slot: 'action',
align: 'center',
render: (row) => {
return [
withDirectives(
h(
NButton,
{
text: true,
onClick: () => {
openModal(2, row)
},
},
{
default: () => '编辑',
}
),
[[vPerms, ['/admin/auth/menu/set']]]
),
]
},
},
])
const data = ref([])
onMounted(() => {
getList()
})
const getList = async () => {
loading.value = true
const res = await api.getMenuList()
data.value = res.data.data || []
loading.value = false
}
const model = ref({
id: null,
pid: null,
type: null,
icon: '',
status: 1,
is_show: 1,
name: '',
route: '',
params: '',
sort: 0,
components: '',
api_route: '',
})
const formRef = ref(null)
const rules = {
pid: {
required: true,
type: 'number',
message: '请选择菜单目录',
trigger: 'change',
},
type: {
required: true,
type: 'number',
message: '请选择菜单类型',
trigger: 'change',
},
name: {
required: true,
type: 'string',
message: '请填写菜单名称',
trigger: 'blur',
},
route: {
required: true,
type: 'string',
message: '请填写菜单路径',
trigger: 'blur',
},
components: {
required: true,
message: '请填写组件路径',
trigger: 'blur',
},
}
const openModal = (type, row = {}) => {
// get_dir_list()
if (type === 2) {
model.value = {
id: row.ID,
type: row.type,
icon: row.icon,
status: row.status,
is_show: row.is_show,
name: row.name,
route: row.route,
params: row.params,
sort: row.sort,
pid: row.pid,
components: row.components,
api_route: row.api_route,
}
}
showModal.value = true
}
const clickIcon = (icon) => {
model.value.icon = icon
iconModal.value = false
}
const handleValidateClick = () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
const dataObj = {
ID: model.value.id || '',
Icon: model.value.icon,
Type: model.value.type,
Status: model.value.status,
IsShow: model.value.is_show,
Name: model.value.name,
Route: model.value.route,
Params: model.value.params,
Sort: model.value.sort,
Pid: model.value.pid || '',
Components: model.value.components || '',
ApiRoute: model.value.api_route || '',
}
const res = await api.addMenu(dataObj)
$message.success(res.msg)
clear()
} catch (error) {
$message.error(error.msg)
throw error
}
}
})
}
const clear = () => {
formRef.value?.restoreValidation()
model.value = {
pid: null,
type: null,
icon: '',
status: 1,
isShow: 1,
name: '',
route: '',
params: '',
components: '',
sort: 0,
}
showModal.value = false
getList()
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,221 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-button v-perms="['/admin/auth/set']" type="primary" @click="openModal(1)">添加角色</n-button>
<n-data-table
class="mt-5"
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<n-modal v-model:show="showModal">
<n-card
style="width: 600px"
title="添加/编辑角色"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<!-- {{ model.AuthId }} -->
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="16" label="角色名称:" path="Name">
<n-input v-model:value="model.Name" placeholder="请输入角色名称" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="角色权限:" path="AuthId">
<n-tree
:data="menus"
:default-checked-keys="model.AuthId"
label-field="name"
value-field="ID"
key-field="ID"
children-field="subMenu"
checkable
cascade
virtual-scroll
@update:checked-keys="updateCheckedKeys"
@update:indeterminate-keys="updateIndeterminateKeys"
/>
</n-form-item-gi>
<n-form-item-gi :span="16" label="角色状态:" path="Status">
<n-switch v-model:value="model.Status" :unchecked-value="2" :checked-value="1" />
</n-form-item-gi>
<n-form-item-gi :span="18">
<div m-auto>
<n-button type="primary" @click="handleValidateClick">保存</n-button>
<n-button ml-10 @click="clear">取消</n-button>
</div>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import api from '../api'
import { NTag, NButton } from 'naive-ui'
import { h, withDirectives, resolveDirective } from 'vue'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const columns = ref([
{
title: '角色名称',
key: 'name',
align: 'center',
},
{
title: '角色状态',
slot: 'status',
align: 'center',
render: (row) => {
return h(
NTag,
{
type: row.status === 1 ? 'success' : 'warning',
},
{
default: () => (row.status === 1 ? '启用' : '禁用'),
}
)
},
},
{
title: '操作',
slot: 'action',
align: 'center',
render: (row) => {
return [
withDirectives(
h(
NButton,
{
text: true,
onClick: () => {
openModal(2, row)
},
},
{
default: () => '编辑',
}
),
[[vPerms, ['/admin/auth/set']]]
),
]
},
},
])
const data = ref([])
const showModal = ref(false)
const model = ref({
Name: '',
AuthId: [],
Status: 2,
})
const rules = {
Name: {
required: true,
message: '请输入角色名称',
trigger: 'blur',
},
AuthId: {
required: true,
type: 'array',
message: '请选择角色权限',
trigger: ['blur', 'input'],
},
}
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
})
onMounted(() => {
getList()
get_menu_list()
})
const getList = async () => {
loading.value = true
try {
const res = await api.getRoleList({
pageNum: 1,
pageSize: 10,
})
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
throw error
}
loading.value = false
}
const menus = ref([])
const get_menu_list = async () => {
const res = await api.getMenuList()
menus.value = res.data.data
}
const openModal = (type, row = {}) => {
if (type === 2) {
model.value = {
ID: row.ID,
Name: row.name,
AuthId: JSON.parse(row.auth_id),
Status: row.status,
}
}
showModal.value = true
}
const clear = () => {
formRef.value?.restoreValidation()
model.value = {
Name: '',
AuthId: [],
Status: 2,
}
showModal.value = false
getList()
}
const formRef = ref(null)
const handleValidateClick = () => {
formRef.value?.validate(async (valid) => {
if (!valid) {
const res = await api.addRole(model.value)
$message.success(res.msg)
clear()
}
})
}
const updateCheckedKeys = (value) => {
model.value.AuthId = value
}
const updateIndeterminateKeys = (value) => {
model.value.AuthId = [...model.value.AuthId, ...value]
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,297 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-button v-perms="['/admin/manage/set']" type="primary" @click="openModal(1)">
添加管理员
</n-button>
<n-data-table
class="mt-5"
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<n-modal v-model:show="showModal">
<n-card
style="width: 600px"
title="添加/编辑角色"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="16" label="名称:" path="Name">
<n-input v-model:value="model.Name" placeholder="请输入管理员名称" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="账号:" path="Phone">
<n-input v-model:value="model.Phone" placeholder="请输入管理员账号(手机号码)" />
</n-form-item-gi>
<n-form-item-gi v-if="nowType === 1" :span="16" label="密码:" path="Password">
<n-input v-model:value="model.Password" placeholder="请输入管理员密码" />
</n-form-item-gi>
<n-form-item-gi v-else :span="16" label="新密码:">
<n-input v-model:value="model.Password" placeholder="不修改请留空" />
</n-form-item-gi>
<n-form-item-gi :span="16" label="角色:" path="AuthId">
<n-select
v-model:value="model.AuthId"
multiple
:options="roles"
label-field="name"
value-field="ID"
/>
</n-form-item-gi>
<n-form-item-gi :span="16" label="角色状态:" path="Status">
<n-switch v-model:value="model.Status" :unchecked-value="2" :checked-value="1" />
</n-form-item-gi>
<n-form-item-gi :span="18">
<div m-auto>
<n-button type="primary" @click="handleValidateClick">保存</n-button>
<n-button ml-10 @click="clear">取消</n-button>
</div>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import { h, withDirectives, resolveDirective } from 'vue'
import api from '../api'
import { NTag, NButton } from 'naive-ui'
const vPerms = resolveDirective('perms')
const loading = ref(false)
const columns = ref([
{
title: '管理员名称',
key: 'name',
align: 'center',
},
{
title: '账号',
key: 'phone',
align: 'center',
},
{
title: '角色',
slot: 'auth_id',
align: 'center',
render: (row) => {
const nowRow = { ...row, auth_id: JSON.parse(row.auth_id) }
const roleName = []
roles.value.forEach((item) => {
nowRow.auth_id.forEach((itm) => {
if (item.ID === itm) {
roleName.push(item.name)
}
})
})
return h(
'span',
{},
{
default: () => roleName.join(' | '),
}
)
},
},
{
title: '角色状态',
slot: 'status',
align: 'center',
render: (row) => {
return h(
NTag,
{
type: row.status === 1 ? 'success' : 'warning',
},
{
default: () => (row.status === 1 ? '启用' : '禁用'),
}
)
},
},
{
title: '操作',
slot: 'action',
align: 'center',
render: (row) => {
return [
withDirectives(
h(
NButton,
{
text: true,
onClick: () => {
openModal(2, row)
},
},
{
default: () => '编辑',
}
),
[[vPerms, ['/admin/manage/set']]]
),
withDirectives(
h(
NButton,
{
class: 'ml-10',
type: 'error',
text: true,
onClick: () => {
delVerifyUser(row)
},
},
{
default: () => '删除',
}
),
[[vPerms, ['/admin/manage/delete']]]
),
]
},
},
])
const data = ref([])
const showModal = ref(false)
const model = ref({
Name: '',
AuthId: null,
Phone: '',
Password: '',
Status: 2,
})
const rules = {
Name: {
required: true,
message: '请输入角色名称',
trigger: 'blur',
},
AuthId: {
required: true,
type: 'array',
message: '请选择分配权限',
trigger: ['blur'],
},
Phone: {
required: true,
message: '请输入管理员账号(手机号码)',
trigger: 'blur',
},
Password: {
required: true,
message: '请输入管理员密码',
trigger: 'blur',
},
}
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
})
onMounted(() => {
getList()
get_role_list()
})
const getList = async () => {
loading.value = true
try {
const res = await api.getAdminList({
pageNum: 1,
pageSize: 10,
})
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
throw error
}
loading.value = false
}
const roles = ref([])
const get_role_list = async () => {
const res = await api.getRoleList()
roles.value = res.data.data || []
}
const nowType = ref(1)
const openModal = (type, row = {}) => {
nowType.value = type
get_role_list()
if (type === 2) {
model.value = {
ID: row.ID,
Name: row.name,
AuthId: JSON.parse(row.auth_id),
Status: row.status,
Phone: row.phone,
}
}
showModal.value = true
}
const clear = () => {
formRef.value?.restoreValidation()
model.value = {
Name: '',
AuthId: null,
Phone: '',
Password: '',
Status: 2,
}
showModal.value = false
getList()
}
const formRef = ref(null)
const handleValidateClick = () => {
formRef.value?.validate(async (valid) => {
if (!valid) {
const res = await api.addAdmin(model.value)
$message.success(res.msg)
clear()
}
})
}
const delVerifyUser = async (row) => {
$dialog.error({
title: '提示',
content: '删除无法撤销,请谨慎!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
const res = await api.delVerifyUser({
uid: row.uid,
})
$message.success(res.msg)
getList()
},
})
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,24 @@
import { request } from '@/utils'
export default {
// 获取权限列表
getMenuList: () => request.post('/auth/menu'),
// 获取菜单目录
getDirList: (data) => request.post('/auth/menu/parent', data),
// 添加/修改菜单
addMenu: (data) => request.post('/auth/menu/set', data),
// 获取角色列表
getRoleList: (data) => request.post('/auth', data),
// 添加/修改角色
addRole: (data) => request.post('/auth/set', data),
// 获取管理员列表
getAdminList: (data) => request.post('/manage', data),
// 添加/修改管理员
addAdmin: (data) => request.post('/manage/set', data),
// 删除核销人员
delVerifyUser: (data) => request.post('/manage/delete', data),
}

View File

@@ -1,12 +1,15 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-button type="primary" @click="handleAdd(1)">新增幻灯片</n-button>
<n-button v-perms="['/admin/rotation/edit']" type="primary" @click="handleAdd(1)">
新增幻灯片
</n-button>
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
<n-modal v-model:show="showModal">
<n-card
@@ -44,10 +47,11 @@
</template>
<script setup>
import { onMounted, h } from 'vue'
import { onMounted, h, withDirectives, resolveDirective } from 'vue'
import api from './api'
import { NButton } from 'naive-ui'
import { NButton, NImage } from 'naive-ui'
import Upload from '@/components/Upload.vue'
const vPerms = resolveDirective('perms')
const loading = ref(false)
@@ -62,12 +66,9 @@ const columns = ref([
align: 'center',
slot: 'url',
render(row) {
return h('img', {
return h(NImage, {
width: '50',
src: row.url[0]?.url || '',
style: {
width: '30px',
height: '30px',
},
})
},
},
@@ -85,17 +86,20 @@ const columns = ref([
slot: 'action',
render(row) {
return [
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
formValue.value = row
handleAdd(2)
withDirectives(
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
formValue.value = row
handleAdd(2)
},
},
},
() => '编辑'
() => '编辑'
),
[[vPerms, ['/admin/rotation/edit']]]
),
]
},

View File

@@ -5,6 +5,11 @@ export default {
path: '/sys',
component: Layout,
redirect: '/sys_banner',
meta: {
title: '系统管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'Sysbanner',
@@ -26,5 +31,47 @@ export default {
order: 10,
},
},
{
name: 'SysAcc',
path: 'sys_acc',
redirect: '/sys_acc_user',
meta: {
title: '权限管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'SysAccUser',
path: 'sys_acc_user',
component: () => import('./acc/acc_user/index.vue'),
meta: {
title: '账号管理',
icon: 'mdi:account-multiple',
order: 10,
},
},
{
name: 'SysAccRole',
path: 'sys_acc_role',
component: () => import('./acc/acc_role/index.vue'),
meta: {
title: '角色管理',
icon: 'mdi:account-multiple',
order: 10,
},
},
{
name: 'SysAccMenu',
path: 'sys_acc_menu',
component: () => import('./acc/acc_menu/index.vue'),
meta: {
title: '菜单权限',
icon: 'mdi:account-multiple',
order: 10,
},
},
],
},
],
}

View File

@@ -15,7 +15,14 @@
/>
</n-form-item>
<n-form-item>
<n-button attr-type="button" @click="handleValidateClick">保存</n-button>
<n-button
v-perms="['/admin/userConfig/edit']"
attr-type="button"
type="primary"
@click="handleValidateClick"
>
保存
</n-button>
</n-form-item>
</n-form>
</CommonPage>

View File

@@ -2,4 +2,18 @@ import { request } from '@/utils'
export default {
getUser: (data) => request.post('/user', data),
// 获取活动订单
gethdlist: (data) => request.post('/user/order', data),
// 获取积分订单
getjflist: (data) => request.post('/user/point/order', data),
// 获取豆子记录
getdzJllist: (data) => request.post('/user/pluse', data),
// 获取积分记录
getjfJllist: (data) => request.post('/user/point', data),
// 获取推广记录
gettgJllist: (data) => request.post('/user/referee/point', data),
// 积分赠送
addUserPulse: (data) => request.post('/gift/pulse', data),
// 获取赠送记录
getgiftJllist: (data) => request.post('/user/gift/pluse', data),
}

View File

@@ -1,33 +1,345 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-grid class="mb-10" x-gap="12">
<n-gi :span="24">
<div flex>
<n-card w-250>
<n-statistic label="用户总赢积分" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.win" />
</n-statistic>
</n-card>
<n-card ml-10 w-250>
<n-statistic label="用户积分(留存)" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.integral" />
</n-statistic>
</n-card>
<n-card ml-10 w-250>
<n-statistic label="总豆子" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.pulse" />
</n-statistic>
</n-card>
<n-card ml-10 w-250>
<n-statistic label="今日新增用户" tabular-nums>
<n-number-animation
ref="numberAnimationInstRef"
:from="0"
:to="cardData.today_user"
/>
</n-statistic>
</n-card>
<n-card ml-10 w-250>
<n-statistic label="用户总流水(元)" tabular-nums>
<n-number-animation
ref="numberAnimationInstRef"
:from="0"
:to="cardData.total_number"
:precision="2"
/>
</n-statistic>
</n-card>
<n-card ml-10 w-250>
<n-statistic label="总佣金(积分)" tabular-nums>
<n-number-animation
ref="numberAnimationInstRef"
:from="0"
:to="cardData.referee"
:precision="2"
/>
</n-statistic>
</n-card>
<n-card ml-10 w-250>
<n-statistic label="平台总用户" tabular-nums>
<n-number-animation
ref="numberAnimationInstRef"
:from="0"
:to="cardData.total_user"
/>
</n-statistic>
</n-card>
</div>
</n-gi>
<n-gi span="12" mt-10 flex items-center>
<span w-100>筛选条件:</span>
<n-input-group>
<n-select
v-model:value="queryParams.selectKey"
:style="{ width: '20%' }"
:options="selectOptions"
placeholder="请选择"
/>
<n-input v-model:value="queryParams.word" :style="{ width: '30%' }" />
</n-input-group>
</n-gi>
<n-gi :span="24" mt-10>
<div>
<span>筛选状态</span>
<n-radio-group v-model:value="queryParams.status">
<n-radio-button
v-for="song in songs"
:key="song.value"
:value="song.value"
:label="song.label"
/>
</n-radio-group>
</div>
</n-gi>
<n-gi :span="24" mt-10>
<div>
<span>活动赠送</span>
<n-radio-group v-model:value="queryParams.type">
<n-radio-button
v-for="song in songs1"
:key="song.value"
:value="song.value"
:label="song.label"
/>
</n-radio-group>
</div>
</n-gi>
<n-gi :span="10">
<div mt-10 flex items-center>
<span w-100>时间筛选</span>
<n-date-picker
v-model:formatted-value="queryParams.time"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
clearable
/>
</div>
</n-gi>
<n-gi span="24" mt-10 flex items-center>
<n-button type="primary" @click="getList">查询</n-button>
<n-button ml-10 @click="clear">重置</n-button>
</n-gi>
</n-grid>
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
@update:sorter="handleSorterChange"
/>
<!-- 用户详情 -->
<n-drawer v-model:show="isDrawer" :width="1000" placement="right" :mask-closable="false">
<n-drawer-content title="用户详情" closable>
<div flex items-center>
<img rounded-full :src="nowRow.avatarUrl" width="70" alt="avatarUrl" />
<div ml-10>
<div>昵称{{ nowRow.nickName }}</div>
<div>电话{{ nowRow.phone }}</div>
</div>
</div>
<div mt-10 w-200 flex items-center justify-between text-center>
<div>
<div>用户积分</div>
<div text-red>{{ nowRow.integral }}</div>
</div>
<div>
<div>用户豆子</div>
<div text-red>{{ nowRow.pulse }}</div>
</div>
</div>
<n-tabs v-model:value="tabVal" type="line" animated @update-value="tabsChange">
<n-tab name="1" tab="活动订单"></n-tab>
<!-- <n-tab name="2" tab="积分订单"></n-tab>-->
<n-tab name="3" tab="豆子记录"></n-tab>
<n-tab name="4" tab="积分记录"></n-tab>
<n-tab name="5" tab="推广记录"></n-tab>
<n-tab name="6" tab="赠送记录"></n-tab>
</n-tabs>
<n-row gutter="12">
<n-col :span="12">
<div mt-10 flex items-center>
<span w-100>时间筛选</span>
<n-date-picker
v-model:formatted-value="queryData.time"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
clearable
/>
</div>
</n-col>
<n-col v-if="tabVal === '4'" :span="10">
<div mt-10 flex items-center>
<span w-100>条件筛选</span>
<n-select
v-model:value="queryData.selectKey"
:style="{ width: '30%' }"
:options="[
{
label: '取消订单',
value: 1,
},
{
label: '支付订单',
value: 2,
},
{
label: '商品赠送',
value: 3,
},
]"
placeholder="请选择类型"
/>
</div>
</n-col>
<n-col :span="4">
<div mt-10>
<n-button type="primary" @click="getTabsList">搜索</n-button>
<n-button ml-10 @click="tabsClear">重置</n-button>
</div>
</n-col>
</n-row>
<n-data-table
class="mt-5"
:columns="tabsColumns"
:loading="tabsLoading"
:data="tabsData"
:pagination="tabsPagination"
:bordered="false"
remote
/>
</n-drawer-content>
</n-drawer>
<!-- 赠送 -->
<n-modal v-model:show="showModal" :mask-closable="false">
<n-card
style="width: 600px"
title="豆子赠送"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="24" label="豆子" path="inputValue">
<n-input-number
v-model:value="model.Number"
:default-value="0"
:min="0"
:step="100"
/>
</n-form-item-gi>
<n-form-item-gi :span="24">
<n-button type="primary" @click="ok">提交</n-button>
<n-button ml-10 @click="addClear">取消</n-button>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
</n-modal>
</CommonPage>
</template>
<script setup>
import { h } from 'vue'
import api from './api'
import { NDropdown, NButton } from 'naive-ui'
import TheIcon from '@/components/icon/TheIcon.vue'
const loading = ref(false)
const queryParams = ref({
word: '',
selectKey: null,
status: '',
time: null,
type: '',
})
const formRef = ref(null)
const nowUserRow = ref({})
const showModal = ref(false)
const model = ref({})
const rules = {}
const queryData = ref({
time: null,
selectKey: 2,
})
const cardData = ref({
integral: 0,
pulse: 0,
today_user: 0,
total_number: 0,
total_user: 0,
win: 0,
})
const songs = ref([
{
value: 1,
label: '未使用',
},
{
value: 3,
label: '用户赢',
},
{
value: 4,
label: '用户输',
},
{
value: 5,
label: '已过期',
},
])
const songs1 = ref([
{
value: 5,
label: '注册赠送',
},
{
value: 6,
label: '签到赠送',
},
{
value: 7,
label: '主动赠送',
},
])
const selectOptions = [
{
label: '用户昵称',
value: 0,
},
{
label: '用户电话',
value: 1,
},
]
const isDrawer = ref(false)
const columns = ref([
{
title: 'ID',
align: 'center',
key: 'ID',
},
{
title: '昵称',
align: 'center',
key: 'nickName',
},
{
title: '头像',
align: 'center',
slot: 'avatar',
render(row) {
return h('img', {
src: row.avatar,
src: row.avatarUrl,
style: {
width: '30px',
height: '30px',
@@ -45,18 +357,74 @@ const columns = ref([
title: '用户积分',
align: 'center',
key: 'integral',
sorter: true,
sortOrder: false,
},
{
title: '赢积分',
align: 'center',
key: 'win',
sorter: true,
sortOrder: false,
},
{
title: '用户豆子',
align: 'center',
key: 'pulse',
sorter: true,
sortOrder: false,
},
{
title: '操作',
align: 'center',
slot: 'action',
render(row) {
console.log(row)
return [
h(
NDropdown,
{
trigger: 'click',
options: [
{
label: '用户详情',
key: 1,
},
{
label: '豆子赠送',
key: 2,
},
],
onSelect: (key) => {
nowUserRow.value = { ...row }
switch (key) {
case 1:
openDrawer(nowUserRow.value)
break
case 2:
showModal.value = true
break
}
},
},
{
default: () =>
h(
NButton,
{
text: true,
iconPlacement: 'right',
},
{
default: () => '更多',
icon: () =>
h(TheIcon, {
icon: 'ant-design:down-outlined',
}),
}
),
}
),
]
},
},
])
@@ -64,9 +432,9 @@ const columns = ref([
const data = ref([])
const pagination = ref({
current: 1,
page: 1,
pageSize: 10,
total: 0,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
@@ -82,20 +450,366 @@ onMounted(() => {
getList()
})
const clear = () => {
queryParams.value = {
word: '',
selectKey: null,
status: '',
time: null,
type: '',
}
getList()
}
const getList = async () => {
loading.value = true
try {
const query_data = {
Status: queryParams.value.status,
Type: queryParams.value.type,
StartTime: queryParams.value.time === null ? '' : queryParams.value.time[0] || '',
EndTime: queryParams.value.time === null ? '' : queryParams.value.time[1] || '',
}
switch (queryParams.value.selectKey) {
case 0:
query_data['UserName'] = queryParams.value.word
break
case 1:
query_data['Phone'] = queryParams.value.word
break
}
const res = await api.getUser({
pageNum: pagination.value.current,
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
...query_data,
})
console.log(res)
data.value = res.data.data
data.value = res.data.data || []
pagination.value.itemCount = res.data.total_user
cardData.value.today_user = res.data.today_user
cardData.value.total_user = res.data.total_user
cardData.value.total_number = res.data.total_number
cardData.value.integral = res.data.integral
cardData.value.pulse = res.data.pulse
cardData.value.referee = res.data.referee
cardData.value.win = res.data.total_win
} catch (error) {
$message.error(error.msg)
}
loading.value = false
}
const nowRow = ref({})
const tabVal = ref('1')
const tabsLoading = ref(false)
const tabsColumns = ref([])
const tabsData = ref([])
const tabsPagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
tabsPagination.value.page = page
getTabsList()
},
})
const openDrawer = (row) => {
nowRow.value = row
isDrawer.value = true
// getTabsList()
tabsChange()
}
const tabsChange = async (e = '1') => {
tabVal.value = e
tabsData.value = []
tabsColumns.value = []
if (tabVal.value === '1' || tabVal.value === '2') {
tabsColumns.value = [
{
title: '订单号',
align: 'center',
key: 'oid',
},
// {
// title: '商品封面',
// align: 'center',
// slot: 'cover',
// render: (row) => {
// return h('img', {
// src: row.cover,
// style: {
// width: '50px',
// height: '50px',
// },
// })
// },
// },
{
title: '商品名称',
align: 'center',
slot: 'goods_name',
render: (row) => {
const el = []
row.OrderGoods.forEach((item) => {
el.push(
h(
'div',
{},
{
default: () =>
`${item.Goods.name}|${item.pay_price}元或${item.pay_integral}积分|X${item.number}`,
}
)
)
})
return el
},
},
{
title: '订单总价',
align: 'center',
slot: 'number',
render: (row) => h('span', row.pay_type === 1 ? `${row.price}` : `${row.exchange}积分`),
},
{
title: '支付方式',
align: 'center',
slot: 'pay_type',
render: (row) => h('span', row.pay_type === 1 ? '微信' : '积分'),
},
{
title: '订单状态',
align: 'center',
slot: 'status',
render: (row) => {
let nameStr = ''
switch (row.status) {
case 0:
nameStr = '待付款'
break
case 1:
nameStr = '待使用'
break
case 2:
nameStr = '已使用'
break
case 3:
nameStr = '已过期'
}
return h(
'span',
{},
{
default: () => nameStr,
}
)
},
},
{
title: '下单时间',
align: 'center',
key: 'add_time',
},
]
} else if (tabVal.value === '3') {
tabsColumns.value = [
{
title: '订单号',
align: 'center',
key: 'oid',
},
{
title: '消费金额',
align: 'center',
key: 'number',
},
{
title: '下单时间',
align: 'center',
key: 'add_time',
},
]
} else if (tabVal.value === '4') {
tabsColumns.value = [
{
title: '订单号',
align: 'center',
key: 'oid',
},
{
title: '积分',
align: 'center',
key: 'number',
},
{
title: '时间',
align: 'center',
key: 'add_time',
},
]
} else if (tabVal.value === '5') {
tabsColumns.value = [
{
title: '订单号',
align: 'center',
key: 'oid',
},
{
title: '用户昵称',
align: 'center',
key: 'nick_name',
},
{
title: '获得积分',
align: 'center',
key: 'number',
},
{
title: '时间',
align: 'center',
key: 'add_time',
},
]
} else if (tabVal.value === '6') {
tabsColumns.value = [
{
title: '获取豆子',
align: 'center',
key: 'number',
},
{
title: '类型',
align: 'center',
slot: 'type',
render: (row) => {
let nameStr = ''
switch (row.type) {
case 5:
nameStr = '注册赠送'
break
case 6:
nameStr = '签到赠送'
break
case 7:
nameStr = '主动赠送'
break
}
return h(
'span',
{},
{
default: () => nameStr,
}
)
},
},
{
title: '获得时间',
align: 'center',
key: 'add_time',
},
]
}
tabsPagination.value.page = 1
await getTabsList()
}
const getTabsList = async () => {
tabsLoading.value = true
let res
const data = {
uid: nowRow.value.uid,
pageNum: tabsPagination.value.page,
pageSize: tabsPagination.value.pageSize,
StartTime: queryData.value.time === null ? '' : queryData.value.time[0] || '',
EndTime: queryData.value.time === null ? '' : queryData.value.time[1] || '',
}
switch (tabVal.value) {
case '1':
res = await api.gethdlist(data)
break
case '2':
res = await api.getjflist(data)
break
case '3':
res = await api.getdzJllist(data)
break
case '4':
data['Type'] = queryData.value.selectKey
res = await api.getjfJllist(data)
break
case '5':
res = await api.gettgJllist(data)
break
case '6':
res = await api.getgiftJllist(data)
break
}
tabsData.value = res.data.data || []
tabsPagination.value.itemCount = res.data.total
tabsLoading.value = false
}
const handleSorterChange = (sorter) => {
if (!loading.value) {
loading.value = true
switch (sorter.columnKey) {
case 'integral':
columns.value[4].sortOrder = !sorter ? false : sorter.order
data.value = data.value.sort((a, b) => {
if (sorter.order === 'descend') return b.integral - a.integral
return a.integral - b.integral
})
break
case 'win':
columns.value[5].sortOrder = !sorter ? false : sorter.order
data.value = data.value.sort((a, b) => {
if (sorter.order === 'descend') return b.win - a.win
return a.win - b.win
})
break
case 'pulse':
columns.value[6].sortOrder = !sorter ? false : sorter.order
data.value = data.value.sort((a, b) => {
if (sorter.order === 'descend') return b.pulse - a.pulse
return a.pulse - b.pulse
})
break
}
loading.value = false
}
}
const tabsClear = async () => {
queryData.value = {
time: null,
selectKey: null,
}
await getTabsList()
}
const ok = () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
await api.addUserPulse({
Uid: nowUserRow.value.uid,
...model.value,
})
addClear()
$message.success('提交成功')
}
})
}
const addClear = () => {
model.value = {
Number: 0,
}
showModal.value = false
getList()
}
</script>
<style lang="scss" scoped></style>

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