release(custom): i

This commit is contained in:
2023-10-10 15:01:00 +08:00
parent a9707c6d94
commit 5b33290de6
78 changed files with 13003 additions and 5230 deletions

4
.env
View File

@@ -2,8 +2,4 @@ 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'

View File

@@ -9,3 +9,9 @@ VITE_USE_PROXY = true
# base api
VITE_BASE_API = '/admin'
VITE_WS1_URL = 'game.wanzhuanyongcheng.cn/dice/home'
VITE_WS_URL = 'test.wanzhuanyongcheng.cn/admin/data'
VITE_MER_LOGIN_URL = '//localhost:3100/#/login'

View File

@@ -13,4 +13,11 @@ VITE_BASE_API = 'https://www.wanzhuanyongcheng.cn/admin'
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 = '//www.wanzhuanyongcheng.cn/static/mer/#/login'

View File

@@ -1,7 +1,18 @@
VITE_PUBLIC_PATH = '/'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/static/admin'
# 是否启用MOCK
VITE_USE_MOCK = true
VITE_USE_MOCK = false
# 是否启用代理
VITE_USE_PROXY = false
# base api
VITE_BASE_API = '/api'
VITE_BASE_API = '/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 = '//test.wanzhuanyongcheng.cn/static/mer/#/login'

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

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>

65
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,65 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_QUOTE_STYLE" value="Single" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

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>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/admin.iml" filepath="$PROJECT_DIR$/.idea/admin.iml" />
</modules>
</component>
</project>

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

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

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

@@ -31,28 +31,30 @@
]
},
"dependencies": {
"@unocss/eslint-config": "^0.55.0",
"@vueuse/core": "^10.3.0",
"@unocss/eslint-config": "^0.55.7",
"@vueuse/core": "^10.5.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"axios": "^1.5.1",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"lodash-es": "^4.17.21",
"md-editor-v3": "^4.2.2",
"md-editor-v3": "^4.7.0",
"mockjs": "^1.1.0",
"pinia": "^2.1.6",
"vite": "^4.4.9",
"vite": "^4.4.11",
"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/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.7.0",
"@iconify/json": "^2.2.100",
"@iconify/json": "^2.2.126",
"@iconify/vue": "^4.1.1",
"@unocss/preset-rem-to-px": "^0.55.0",
"@vitejs/plugin-vue": "^4.2.3",
"@unocss/preset-rem-to-px": "^0.55.7",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
"@zclzone/eslint-config": "^0.0.4",
"chalk": "^5.3.0",
@@ -63,17 +65,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.0",
"unocss": "0.55.0",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"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"
}
}

9555
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",

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>

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,7 +2,7 @@ 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'
const isHash = true
export const router = createRouter({
@@ -17,16 +17,6 @@ export async function setupRouter(app) {
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 +28,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 +37,7 @@ export async function addDynamicRoutes() {
router.addRoute(NOT_FOUND_ROUTE)
} catch (error) {
console.error(error)
throw error
}
}
@@ -58,8 +47,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,105 @@
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,
}
let redirect = ''
if (route.route === '/' && firstRoute) {
// 重定向
redirect = route.subMenu[0].route
}
const curRoute = {
path: route.route,
name: route.name,
isHidden,
meta,
redirect,
children: [],
}
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 +120,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('/dice')) {
// config.baseURL = import.meta.env.VITE_GAME_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,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/store/classify/edit']]]
),
]
},
@@ -105,6 +112,7 @@ const rules = {
}
const formValue = ref({
ID: 0,
name: '',
status: 1,
})
@@ -112,9 +120,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 +141,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 +156,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 +174,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="请输入商户座机" />
@@ -88,13 +92,12 @@
: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 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"
@@ -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,12 @@
</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 === '编辑商户' ? true : false))
const columns = ref([
{
@@ -177,19 +182,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 +237,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,21 +259,6 @@ 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: '',
@@ -323,42 +341,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 +370,7 @@ const getMertype = async () => {
const clearQuryVal = () => {
QuryVal.value = {
StoreName: '',
Status: '',
Status: null,
}
getList()
}
@@ -384,14 +380,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) => {

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,209 @@
<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(','),
}
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.classId) 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

@@ -8,33 +8,214 @@
: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_number">
<n-input-number
v-model:value="nowRow.pulse_number"
clearable
:precision="2"
placeholder="请输入赠送豆子数量...."
/>
</n-form-item-gi>
<n-form-item-gi :span="20" label="赠送积分" path="integral">
<n-input-number
v-model:value="nowRow.integral"
clearable
:precision="2"
placeholder="请输入赠送积分数量...."
/>
</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="请输入分佣比例...."
/>
</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_number: {
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',
},
}
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',
})
},
},
@@ -44,7 +225,7 @@ const columns = ref([
align: 'center',
},
{
title: '商品价格',
title: '商品价格(元)',
key: 'number',
align: 'center',
},
@@ -53,52 +234,130 @@ const columns = ref([
key: 'stock',
align: 'center',
},
{
title: '赠送豆子',
key: 'pulse_number',
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 +368,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 +390,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',

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,17 @@
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),
}

View File

@@ -1,7 +1,7 @@
<template>
<CommonPage show-footer :title="$route.title">
<div flex>
<div flex>
<div flex items-center>
<div mr-20 flex>
<div>游戏状态</div>
<n-switch
v-model:value="val1"
@@ -10,7 +10,11 @@
@update:value="handleUpdateValue1"
/>
</div>
<div ml-20 flex>
<div flex items-center>
<div>开奖记录</div>
<n-button type="primary" @click="openData">预览</n-button>
</div>
<!-- <div ml-20 flex>
<div>点杀状态</div>
<n-switch
v-model:value="val"
@@ -18,55 +22,119 @@
:unchecked-value="2"
@update:value="handleUpdateValue"
/>
</div>
</div> -->
</div>
<div flex>
<div>
预开期数
<span text-25>{{ list[0]?.periods || 0 }}</span>
<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>
<div flex flex-wrap justify-between>
<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
v-for="item in list"
:key="item.ID"
class="mb-10 mt-10 w-300 flex-shrink-0 cursor-pointer"
:title="item.name"
style="width: 900px"
title="开奖记录"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<p text-25 op-60 :style="{ color: item.count === 0 ? 'green' : 'red' }">{{ item.count }}</p>
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:max-height="600"
:pagination="false"
:bordered="false"
/>
</n-card>
<div h-0 w-300></div>
<div h-0 w-300></div>
</div>
</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 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
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 = () => {
@@ -87,14 +155,14 @@ ws1.onmessage = (msg) => {
}
onMounted(() => {
get_data()
// get_data()
get_data1()
})
const get_data = async () => {
const res = await api.getDS()
val.value = res.data.diceStatus
}
// 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
@@ -106,10 +174,13 @@ onBeforeUnmount(() => {
const handleUpdateValue = async (e) => {
const res = await api.setDS({
status: e,
status: 1,
id: e,
user_id: getToken(),
Periods: list.value[0]?.Periods,
})
$message.success(res.msg)
get_data()
// get_data()
}
const handleUpdateValue1 = async (e) => {
@@ -119,6 +190,48 @@ const handleUpdateValue1 = async (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
}
}
</script>
<style lang="scss" scoped></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

@@ -0,0 +1,250 @@
<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>
<!-- <n-grid class="mb-10" x-gap="12" :cols="3">
<n-gi>
<n-card>
<n-statistic label="总下注">
<n-number-animation :from="0" :to="TYVal.total" />
</n-statistic>
</n-card>
</n-gi>
<n-gi>
<n-card>
<n-statistic label="总赔付">
<n-number-animation :from="0" :to="TYVal.totalNum" />
</n-statistic>
</n-card>
</n-gi>
<n-gi>
<n-card>
<n-statistic label="总盈利">
<n-number-animation :from="0" :to="TYVal.total / 10 - TYVal.totalNum / 100" />
</n-statistic>
</n-card>
</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',
},
},
// legend: {
// data: ['下注(豆子)', '赔付(积分)'],
// left: 'left',
// type: 'scroll',
// },
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 TYVal = ref({
total: 0,
totalNum: 0,
})
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
TYVal.value = {
total: res.data.total,
totalNum: res.data.totalDices,
}
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)
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, 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>

189
src/views/game/zs/index.vue Normal file
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

@@ -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

@@ -1,25 +1,155 @@
<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">
<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>
<script setup>
import api from './api'
import { NEllipsis } from 'naive-ui'
const loading = ref(false)
const queryData = ref({
status: '',
time: null,
word: '',
selectKey: 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: '用户',
@@ -27,15 +157,46 @@ const columns = ref([
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: 'store_name',
},
// {
// title: '订单佣金(元)',
// align: 'center',
// key: 'commission_number',
// },
{
title: '订单状态',
align: 'center',
@@ -45,20 +206,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 +237,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 +258,52 @@ 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
}
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.isShow" :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,
isShow: 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,
isShow: row.isShow,
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.isShow,
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,14 @@ 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),
}

View File

@@ -1,33 +1,225 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-grid class="mb-10" x-gap="12">
<n-gi :span="24">
<div flex>
<n-card w-500>
<n-statistic label="用户积分(留存)" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.integral" />
</n-statistic>
</n-card>
<n-card ml-10 w-500>
<n-statistic label="用户豆子(留存)" tabular-nums>
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.pulse" />
</n-statistic>
</n-card>
<n-card ml-10 w-500>
<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-500>
<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-500>
<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-500>
<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 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" />
<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-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 :span="10" v-if="tabVal === '4'">
<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>
</CommonPage>
</template>
<script setup>
import { h } from 'vue'
import api from './api'
import { NDropdown, NButton, NEllipsis } from 'naive-ui'
import TheIcon from '@/components/icon/TheIcon.vue'
const loading = ref(false)
const queryParams = ref({
word: '',
selectKey: null,
})
const queryData = ref({
time: null,
selectKey: 2,
})
const cardData = ref({
integral: 0,
pulse: 0,
today_user: 0,
total_number: 0,
total_user: 0,
})
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 +237,59 @@ const columns = ref([
title: '用户积分',
align: 'center',
key: 'integral',
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,
},
],
onSelect: (key) => {
switch (key) {
case 1:
openDrawer(row)
break
}
},
},
{
default: () =>
h(
NButton,
{
text: true,
iconPlacement: 'right',
},
{
default: () => '更多',
icon: () =>
h(TheIcon, {
icon: 'ant-design:down-outlined',
}),
}
),
}
),
]
},
},
])
@@ -64,9 +297,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 +315,290 @@ onMounted(() => {
getList()
})
const clear = () => {
queryParams.value = {
word: '',
selectKey: null,
}
getList()
}
const getList = async () => {
loading.value = true
try {
const query_data = {}
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
} 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) => {
return h(
NEllipsis,
{
style: 'max-width: 240px',
},
{
default: () => row.goods_name,
}
)
},
},
{
title: '商品价格',
align: 'center',
key: 'number',
},
{
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: 'goods_name',
},
{
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: 'goods_name',
},
{
title: '积分',
align: 'center',
key: 'number',
},
{
title: '时间',
align: 'center',
key: 'add_time',
},
]
} else {
tabsColumns.value = [
{
title: '订单号',
align: 'center',
key: 'oid',
},
{
title: '用户昵称',
align: 'center',
key: 'nick_name',
},
{
title: '商品名称',
align: 'center',
key: 'goods_name',
},
{
title: '获得积分',
align: 'center',
key: 'number',
},
{
title: '时间',
align: 'center',
key: 'add_time',
},
]
}
tabsPagination.value.page = 1
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
}
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 'pulse':
columns.value[5].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
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
}
loading.value = false
}
}
const tabsClear = async () => {
queryData.value = {
time: null,
selectKey: null,
}
await getTabsList()
}
</script>
<style lang="scss" scoped></style>

View File

@@ -5,6 +5,11 @@ export default {
path: '/user',
component: Layout,
redirect: '/user_list',
meta: {
title: '用户管理',
icon: 'mdi:account-multiple',
order: 10,
},
children: [
{
name: 'Userlist',
@@ -12,6 +17,7 @@ export default {
component: () => import('./index/index.vue'),
meta: {
title: '用户列表',
icon: 'mdi:account-multiple',
order: 10,
},
},