feat(custom): i

This commit is contained in:
2023-09-08 10:29:31 +08:00
parent 3b3cb7ba34
commit 363270378a
60 changed files with 1234 additions and 1342 deletions

2
.env
View File

@@ -1,3 +1,3 @@
VITE_TITLE = 'Vue Naive Admin'
VITE_TITLE = '捷兑通 - 商家端'
VITE_PORT = 3100

View File

@@ -2,10 +2,10 @@
VITE_PUBLIC_PATH = '/'
# 是否启用MOCK
VITE_USE_MOCK = true
VITE_USE_MOCK = false
# 是否启用MOCK
# 是否启用代理
VITE_USE_PROXY = true
# base api
VITE_BASE_API = '/api'
VITE_BASE_API = '/store'

View File

@@ -1,11 +1,11 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
VITE_PUBLIC_PATH = '/static/mer'
# 是否启用MOCK
VITE_USE_MOCK = true
VITE_USE_MOCK = false
# base api
VITE_BASE_API = '/api'
VITE_BASE_API = 'https://www.wanzhuanyongcheng.cn/store'
# 是否启用压缩
VITE_USE_COMPRESS = true

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

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

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>

12
.idea/mer.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>

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/mer.iml" filepath="$PROJECT_DIR$/.idea/mer.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>

View File

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

217
README.md
View File

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

View File

@@ -6,28 +6,28 @@ export const PROXY_CONFIG = {
* @请求路径 http://localhost:3100/api/user
* @转发路径 http://localhost:8080/user
*/
'/api': {
target: 'http://localhost:8080',
'/store': {
target: 'https://test.wanzhuanyongcheng.cn',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
// rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
/**
* @desc 不替换匹配值
* @请求路径 http://localhost:3100/api/v2/user
* @转发路径 http://localhost:8080/api/v2/user
*/
'/api/v2': {
target: 'http://localhost:8080',
changeOrigin: true,
},
// '/api/v2': {
// target: 'http://localhost:8080',
// changeOrigin: true,
// },
/**
* @desc 替换部分匹配值
* @请求路径 http://localhost:3100/api/v3/user
* @转发路径 http://localhost:8080/user
*/
'/api/v3': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
// '/api/v3': {
// target: 'http://localhost:8080',
// changeOrigin: true,
// rewrite: (path) => path.replace(new RegExp('^/api'), ''),
// },
}

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

@@ -0,0 +1,110 @@
<template>
<div ref="_ref" style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
/>
<Editor
v-model="htmlValue"
style="height: 500px; overflow-y: hidden"
:default-config="editorConfig"
mode="default"
@on-created="handleCreated"
/>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { onBeforeUnmount, shallowRef, ref } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { getToken } from '@/utils'
const props = defineProps({
valueHtml: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:valueHtml'])
const editorRef = shallowRef()
const _ref = ref(null)
const Token = getToken()
const toolbarConfig = {}
const editorConfig = { placeholder: '请输入商品详情...', MENU_CONF: {} }
const htmlValue = computed({
get() {
console.log('get', props.valueHtml)
return props.valueHtml
},
set(value) {
console.log('set', props.valueHtml)
emit('update:valueHtml', value)
},
})
editorConfig.MENU_CONF['uploadImage'] = {
// 其他配置...
// 小于该值就插入 base64 格式(而不上传),默认为 0
fieldName: 'file',
server: '/store/upload',
allowedFileTypes: ['image/*'],
headers: {
Authorization: `Bearer ${Token}`,
},
base64LimitSize: 5 * 1024, // 5kb
// 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒
// 自定义插入图片
customInsert(res, insertFn) {
if (res.code === 200) {
console.log(res)
$message.success('上传成功')
insertFn(res.data.data)
}
},
}
editorConfig.MENU_CONF['uploadVideo'] = {
server: '/store/upload',
fieldName: 'file',
allowedFileTypes: ['video/*'],
headers: {
Authorization: `Bearer ${Token}`,
},
// 单个文件上传成功之后
customInsert(res, insertFn) {
console.log(res)
// 如果成功了,就把视频的地址,插入到编辑器中
insertFn(res.data.data)
},
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
console.log('销毁')
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // 记录 editor 实例,重要!
editor.on('fullScreen', () => {
_ref.value.style.zIndex = 10
})
}
</script>
<style lang="scss" scoped></style>

67
src/components/Upload.vue Normal file
View File

@@ -0,0 +1,67 @@
<template>
<n-upload
v-model:file-list="List"
action="/store/upload"
:headers="headers"
:max="max"
list-type="image-card"
@before-upload="beforeUpload"
@finish="handleFinish"
@error="handleError"
>
点击上传
</n-upload>
</template>
<script setup>
import { getToken } from '@/utils'
const token = getToken()
const prop = defineProps({
list: {
type: Array,
default: () => [],
},
max: {
type: Number,
default: 1,
},
})
const emit = defineEmits(['update:list'])
const headers = ref({
Authorization: `Bearer ${token}`,
})
const List = computed({
get() {
return prop.list
},
set(val) {
emit('update:list', val)
},
})
const handleFinish = ({ file, event }) => {
const res = JSON.parse(event.target.response)
file.url = res.data.data
return file
}
const handleError = () => {
$message.error('上传失败')
}
// 图片上传限制
const beforeUpload = (data) => {
if (!(data.file.file?.type === 'image/png' || data.file.file?.type === 'image/jpeg')) {
$message.error('只能上传png或jpg格式的图片文件,请重新上传')
return false
}
return true
}
</script>
<style lang="scss" scoped></style>

View File

@@ -1,6 +1,6 @@
<template>
<footer f-c-c flex-col text-14 color="#6a6a6a">
<p>
<!-- <p>
Copyright © 2022-present
<a
href="https://github.com/zclzone"
@@ -18,6 +18,6 @@
>
赣ICP备2020015008号-1
</a>
</p>
</p> -->
</footer>
</template>

View File

@@ -4,9 +4,9 @@
<BreadCrumb ml-15 hidden sm:block />
</div>
<div ml-auto flex items-center>
<MessageNotification />
<!-- <MessageNotification /> -->
<ThemeMode />
<GithubSite />
<!-- <GithubSite /> -->
<FullScreen />
<UserAvatar />
</div>
@@ -17,7 +17,7 @@ import BreadCrumb from './components/BreadCrumb.vue'
import MenuCollapse from './components/MenuCollapse.vue'
import FullScreen from './components/FullScreen.vue'
import UserAvatar from './components/UserAvatar.vue'
import GithubSite from './components/GithubSite.vue'
// import GithubSite from './components/GithubSite.vue'
import ThemeMode from './components/ThemeMode.vue'
import MessageNotification from './components/MessageNotification.vue'
// import MessageNotification from './components/MessageNotification.vue'
</script>

View File

@@ -4,7 +4,7 @@ import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
import { getToken, isNullOrWhitespace } from '@/utils'
import { useUserStore, usePermissionStore } from '@/store'
const isHash = import.meta.env.VITE_USE_HASH === 'true'
const isHash = true
export const router = createRouter({
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
routes: basicRoutes,

View File

@@ -1,4 +1,4 @@
const Layout = () => import('@/layout/index.vue')
// const Layout = () => import('@/layout/index.vue')
export const basicRoutes = [
{
@@ -18,42 +18,42 @@ export const basicRoutes = [
},
},
{
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',
},
},
],
},
// {
// 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 = {

View File

@@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
export const useGoodsStore = defineStore('goods', {
state() {
return {
goodType: 'add',
row: {},
}
},
actions: {
setRow(row) {
this.row = row
},
setType(row) {
this.goodType = row
},
},
})

View File

@@ -2,3 +2,4 @@ export * from './app'
export * from './permission'
export * from './tags'
export * from './user'
export * from './goods'

View File

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import { resetRouter } from '@/router'
import { useTagsStore, usePermissionStore } from '@/store'
import { removeToken, toLogin } from '@/utils'
import api from '@/api'
// import api from '@/api'
export const useUserStore = defineStore('user', {
state() {
@@ -26,14 +26,18 @@ export const useUserStore = defineStore('user', {
},
actions: {
async getUserInfo() {
try {
const res = await api.getUser()
const { id, name, avatar, role } = res.data
this.userInfo = { id, name, avatar, role }
return Promise.resolve(res.data)
} catch (error) {
return Promise.reject(error)
const typeMer = localStorage.getItem('type')
this.userInfo = {
role: [typeMer],
}
// try {
// const res = await api.getUser()
// const { id, name, avatar, role } = res.data
// this.userInfo = { id, name, avatar, role }
// return Promise.resolve(res.data)
// } catch (error) {
// return Promise.reject(error)
// }
},
async logout() {
const { resetTags } = useTagsStore()

View File

@@ -1,7 +1,7 @@
import { lStorage } from '@/utils'
import api from '@/api'
const TOKEN_CODE = 'access_token'
const TOKEN_CODE = 'mer_token'
const DURATION = 6 * 60 * 60
export function getToken() {

View File

@@ -28,7 +28,7 @@ export function reqReject(error) {
export function resResolve(response) {
// TODO: 处理不同的 response.headers
const { data, status, config, statusText } = response
if (data?.code !== 0) {
if (data?.code !== 200) {
const code = data?.code ?? status
/** 根据code处理对应的操作并返回处理后的message */

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,43 +0,0 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: 'Test',
path: '/base',
component: Layout,
redirect: '/base/index',
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>

View File

@@ -5,6 +5,7 @@ export default {
path: '/error-page',
component: Layout,
redirect: '/error-page/404',
isHidden: true,
meta: {
title: '错误页',
icon: 'mdi:alert-circle-outline',

View File

@@ -0,0 +1,6 @@
import { request } from '@/utils'
export default {
getGoodClass: (data) => request.post('/goods/class', data),
addGoods: (data) => request.post('/goods/edit', data),
}

View File

@@ -0,0 +1,173 @@
<template>
<CommonPage show-footer :title="$route.title">
<!-- {{ model }} -->
<n-spin size="large" :show="isShowSpin">
<n-form ref="formRef" label-width="100" :model="model" :rules="rules" label-placement="left">
<n-grid :cols="2" :x-gap="24">
<n-form-item-gi :span="12" label="商品分类:" path="class_id">
<n-select
v-model:value="model.class_id"
label-field="name"
value-field="ID"
:options="classList"
placeholder="选择商品分类"
/>
</n-form-item-gi>
<n-form-item-gi :span="12" label="商品名称:" path="name">
<n-input v-model:value="model.name" placeholder="输入商品名称" />
</n-form-item-gi>
<n-form-item-gi :span="12" label="商品封面:" path="cover">
<Upload v-model:list="model.cover" />
</n-form-item-gi>
<n-form-item-gi :span="12" label="商品轮播图:" path="rotation">
<Upload v-model:list="model.rotation" :max="5" />
</n-form-item-gi>
<n-form-item-gi :span="12" label="商品价格:" path="number">
<n-input-number v-model:value="model.number" placeholder="输入商品价格" />
</n-form-item-gi>
<n-form-item-gi :span="12" label="市场价格:" path="market_num">
<n-input-number v-model:value="model.market_num" placeholder="输入市场价格" />
</n-form-item-gi>
<n-form-item-gi :span="12" label="商品库存:" path="stock">
<n-input-number v-model:value="model.stock" placeholder="输入商品库存" />
</n-form-item-gi>
<n-form-item-gi :span="12" label="商品简介:" path="profile">
<n-input v-model:value="model.profile" type="textarea" placeholder="输入商品简介" />
</n-form-item-gi>
<n-form-item-gi :span="12" label="商品详情:" path="details">
<Editor v-model:valueHtml="model.details" />
</n-form-item-gi>
<n-form-item-gi :span="12">
<n-button class="m-auto" type="primary" @click="submit">提交</n-button>
</n-form-item-gi>
</n-grid>
</n-form>
</n-spin>
</CommonPage>
</template>
<script setup>
import Upload from '@/components/Upload.vue'
import Editor from '@/components/Editor.vue'
import api from './api'
import { useGoodsStore } from '@/store/modules/goods'
import { useRouter } from 'vue-router'
const formRef = ref(null)
const model = ref({
name: '',
class_id: null,
cover: [],
rotation: [],
profile: '',
details: '',
stock: null,
number: null,
market_num: null,
})
const rules = {
name: {
required: true,
message: '请输入商品名称',
trigger: 'blur',
},
class_id: {
required: true,
type: 'number',
message: '请选择商品分类',
trigger: 'change',
},
cover: {
required: true,
type: 'array',
message: '请上传商品封面',
trigger: 'change',
},
rotation: {
required: true,
type: 'array',
message: '请上传商品轮播图',
trigger: 'change',
},
number: {
required: true,
type: 'number',
message: '请输入商品价格',
trigger: 'blur',
},
market_num: {
required: true,
type: 'number',
message: '请输入市场价格',
trigger: 'blur',
},
stock: {
required: true,
type: 'number',
message: '请输入商品库存',
trigger: 'blur',
},
}
const { row, setRow, goodType } = useGoodsStore()
const type = ref('')
const isShowSpin = ref(false)
onMounted(() => {
type.value = goodType
getClassList()
})
const classList = ref([])
const getClassList = async () => {
isShowSpin.value = true
const res = await api.getGoodClass()
classList.value = res.data.data
if (row && type.value === 'edit') {
console.log(row)
type.value = 'edit'
model.value = {
...row,
cover: [{ url: row.cover, status: 'finished', id: 1 }],
rotation: row.rotation?.split(',').map((item, index) => ({
url: item,
status: 'finished',
id: index + 1,
})),
}
}
isShowSpin.value = false
}
const route = useRouter()
const submit = () => {
formRef.value.validate(async (errors) => {
if (errors) return
try {
const data = {
...model.value,
cover: model.value.cover.map((item) => item.url)[0],
rotation: model.value.rotation.map((item) => item.url).join(','),
status: 3,
}
const res = await api.addGoods(data)
console.log(res)
// $message.success(res.data.msg)
route.back()
setRow({})
} catch (error) {
$message.error(error.msg)
}
})
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,6 @@
import { request } from '@/utils'
export default {
getGoods: (data) => request.post('/goods', data),
getGoodClass: (data) => request.post('/goods/class', data),
}

View File

@@ -0,0 +1,170 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-button type="primary" @click="addGood(1)">添加商品</n-button>
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
</CommonPage>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { NButton, NEllipsis } from 'naive-ui'
import api from './api'
import { useGoodsStore } from '@/store/modules/goods'
const loading = ref(false)
const columns = ref([
{
title: 'ID',
align: 'center',
key: 'ID',
},
{
title: '封面',
align: 'center',
slot: 'cover',
render(row) {
return h('img', {
src: row.cover,
style: {
width: '30px',
height: '30px',
},
})
},
},
{
title: '商品分类',
align: 'center',
slot: 'class_id',
render(row) {
const data = optList.value.filter((item) => item.ID === row.class_id)
return h('span', data[0]?.name)
},
},
{
title: '商品名称',
align: 'center',
slot: 'name',
render(row) {
return h(
NEllipsis,
{
class: 'w-300',
},
{ default: () => row.name }
)
},
},
{
title: '商品价格',
align: 'center',
key: 'number',
},
{
title: '商品库存',
align: 'center',
key: 'stock',
},
{
title: '状态',
align: 'center',
slot: 'status',
render(row) {
return h('span', row.status === 1 ? '已审核' : row.status === 2 ? '已拒绝' : '待审核')
},
},
{
title: '备注',
key: 'notes',
align: 'center',
},
{
title: '操作',
align: 'center',
slot: 'action',
render(row) {
if (row.status === 1 || row.status === 2) {
return [
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => toEdit(row),
},
() => '编辑'
),
]
}
},
},
])
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
getList()
},
})
onMounted(() => {
getList()
getclasslist()
})
const optList = ref([])
const getclasslist = async () => {
const res = await api.getGoodClass()
optList.value = res.data.data
}
const getList = async () => {
loading.value = true
try {
const res = await api.getGoods({
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
data.value = res.data.data || []
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
}
loading.value = false
}
const route = useRouter()
const { setRow, setType } = useGoodsStore()
const addGood = (type) => {
setType(type === 1 ? 'add' : 'edit')
route.push({
path: '/goods/goods_add',
})
}
const toEdit = (item) => {
setRow(item)
addGood()
}
</script>
<style lang="scss" scoped></style>

31
src/views/goods/route.js Normal file
View File

@@ -0,0 +1,31 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: '商品管理',
path: '/goods',
component: Layout,
redirect: '/goods_list',
children: [
{
name: 'Goodslist',
path: 'goods_list',
component: () => import('./index/index.vue'),
meta: {
title: '商品列表',
icon: 'mdi:account-multiple',
order: 10,
},
},
{
name: 'Goodsadd',
path: 'goods_add',
isHidden: true,
component: () => import('./add/index.vue'),
meta: {
title: '添加/编辑商品',
icon: 'mdi:account-multiple',
order: 10,
},
},
],
}

View File

@@ -1,5 +1,5 @@
import { request } from '@/utils'
export default {
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
login: (data) => request.post('/login', data, { noNeedToken: true }),
}

View File

@@ -19,7 +19,7 @@
v-model:value="loginInfo.name"
autofocus
class="h-50 items-center pl-10 text-16"
placeholder="admin"
placeholder="请输入账号"
:maxlength="20"
/>
</div>
@@ -29,7 +29,7 @@
class="h-50 items-center pl-10 text-16"
type="password"
show-password-on="mousedown"
placeholder="123456"
placeholder="请输入密码"
:maxlength="20"
@keypress.enter="handleLogin"
/>
@@ -99,9 +99,10 @@ async function handleLogin() {
try {
loading.value = true
$message.loading('正在验证...')
const res = await api.login({ name, password: password.toString() })
const res = await api.login({ phone: name, password: password.toString() })
$message.success('登录成功')
setToken(res.data.token)
window.localStorage.setItem('type', res.data.type)
if (isRemember.value) {
lStorage.set('loginInfo', { name, password })
} else {

View File

@@ -1,3 +0,0 @@
<template>
<div>a-1-1</div>
</template>

View File

@@ -1,3 +0,0 @@
<template>
<div>a-1-2</div>
</template>

View File

@@ -1,8 +0,0 @@
<template>
<CommonPage>
<div>a-1</div>
<div pl-20>
<RouterView />
</div>
</CommonPage>
</template>

View File

@@ -1,3 +0,0 @@
<template>
<div>a-2-1</div>
</template>

View File

@@ -1,8 +0,0 @@
<template>
<CommonPage>
<div>a-2</div>
<div pl-20>
<RouterView />
</div>
</CommonPage>
</template>

View File

@@ -1,8 +0,0 @@
<template>
<CommonPage>
<div>a</div>
<div pl-20>
<RouterView />
</div>
</CommonPage>
</template>

View File

@@ -1,75 +0,0 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: 'MultipleMenu',
path: '/multi-menu',
component: Layout,
meta: {
title: '多级菜单',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
order: 4,
},
children: [
{
name: 'a-1',
path: 'multiple-menu',
component: () => import('./a-1/index.vue'),
meta: {
title: 'a-1',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
children: [
{
name: 'a-1-1',
path: 'a-1-1',
component: () => import('./a-1/a-1-1/index.vue'),
meta: {
title: 'a-1-1',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
},
{
name: 'a-1-2',
path: 'a-1-2',
component: () => import('./a-1/a-1-2/index.vue'),
meta: {
title: 'a-1-2',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
},
],
},
{
name: 'a-2',
path: 'a-2',
component: () => import('./a-2/index.vue'),
meta: {
title: 'a-2',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
children: [
{
name: 'a-2-1',
path: 'a-2-1',
component: () => import('./a-2/a-2-1/index.vue'),
meta: {
title: 'a-2-1单个子菜单',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
},
],
},
],
}

View File

@@ -0,0 +1,5 @@
import { request } from '@/utils'
export default {
getOrder: (data) => request.post('/order', data),
}

View File

@@ -0,0 +1,105 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
</CommonPage>
</template>
<script setup>
import api from './api'
const loading = ref(false)
const columns = ref([
{
title: '订单号',
align: 'center',
key: 'oid',
},
{
title: '用户',
align: 'center',
key: 'user_name',
},
{
title: '商品名称',
align: 'center',
key: 'goods_name',
},
{
title: '商品价格',
align: 'center',
key: 'number',
},
{
title: '订单状态',
align: 'center',
slot: 'status',
render(row) {
switch (row.status) {
case 0:
return h('span', '待付款')
case 1:
return h('span', '待使用')
case 2:
return h('span', '已完成')
case 3:
return h('span', '已过期')
}
},
},
{
title: '操作',
align: 'center',
slot: 'action',
render(row) {
console.log(row)
},
},
])
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
getList()
},
})
onMounted(() => {
getList()
})
const getList = async () => {
loading.value = true
try {
const res = await api.getOrder({
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
}
</script>
<style lang="scss" scoped></style>

26
src/views/order/route.js Normal file
View File

@@ -0,0 +1,26 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: 'Order',
path: '/order',
component: Layout,
redirect: '/order_list',
meta: {
title: '订单管理',
icon: 'majesticons:compass-line',
order: 1,
// requireAuth: true,
// role: ['1'],
},
children: [
{
name: 'OrderList',
path: 'order_list',
component: () => import('./index/index.vue'),
meta: {
title: '订单列表',
icon: 'material-symbols:auto-awesome-outline-rounded',
},
},
],
}

View File

@@ -0,0 +1,5 @@
import { request } from '@/utils'
export default {
getJF: (data) => request.post('/record', data),
}

View File

@@ -0,0 +1,75 @@
<template>
<!-- <div> -->
<CommonPage show-footer :title="$route.title">
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
</CommonPage>
<!-- </div> -->
</template>
<script setup>
import api from './api'
const loading = ref(false)
const columns = ref([
{
title: 'ID',
key: 'ID',
align: 'center',
},
{
title: '订单号',
key: 'oid',
align: 'center',
},
{
title: '获取积分',
key: 'number',
align: 'center',
},
])
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
getList()
},
})
onMounted(() => {
getList()
})
const getList = async () => {
loading.value = true
try {
const res = await api.getJF({
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
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,5 @@
import { request } from '@/utils'
export default {
getList: (data) => request.post('/order', data),
}

View File

@@ -0,0 +1,80 @@
<template>
<!-- <div> -->
<CommonPage show-footer :title="$route.title">
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
</CommonPage>
<!-- </div> -->
</template>
<script setup>
import api from './api'
const loading = ref(false)
const columns = ref([
{
title: 'ID',
key: 'ID',
align: 'center',
},
{
title: '商品名称',
key: 'goods_name',
align: 'center',
},
{
title: '用户名称',
key: 'user_name',
align: 'center',
},
{
title: '获取积分',
key: 'number',
align: 'center',
},
])
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
getList()
},
})
onMounted(() => {
getList()
})
const getList = async () => {
loading.value = true
try {
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)
}
loading.value = false
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,35 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: '积分管理',
path: '/settlement',
component: Layout,
redirect: '/settlement_list',
meta: {
requireAuth: true,
order: 1,
role: ['2'],
},
children: [
{
name: 'Settlementlist',
path: 'settlement_list',
component: () => import('./index/index.vue'),
meta: {
title: '积分明细',
icon: 'mdi:account-multiple',
order: 10,
},
},
// {
// name: 'Jflist',
// path: 'jf_list',
// component: () => import('./jf_list/index.vue'),
// meta: {
// title: '积分列表',
// icon: 'mdi:account-multiple',
// order: 10,
// },
// },
],
}

View File

@@ -0,0 +1,5 @@
import { request } from '@/utils'
export default {
getUser: (data) => request.post('/user', data),
}

View File

@@ -0,0 +1,102 @@
<template>
<CommonPage show-footer :title="$route.title">
<n-data-table
:loading="loading"
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
remote
/>
</CommonPage>
</template>
<script setup>
import api from './api'
const loading = ref(false)
const columns = ref([
{
title: 'ID',
align: 'center',
key: 'ID',
},
{
title: '头像',
align: 'center',
slot: 'avatar',
render(row) {
return h('img', {
src: row.avatar,
style: {
width: '30px',
height: '30px',
borderRadius: '50%',
},
})
},
},
{
title: '电话',
align: 'center',
key: 'phone',
},
{
title: '用户积分',
align: 'center',
key: 'integral',
},
{
title: '用户豆子',
align: 'center',
key: 'pulse',
},
{
title: '操作',
align: 'center',
slot: 'action',
render(row) {
console.log(row)
},
},
])
const data = ref([])
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
onChange: (page) => {
pagination.value.page = page
getList()
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
getList()
},
})
onMounted(() => {
getList()
})
const getList = async () => {
loading.value = true
try {
const res = await api.getUser({
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
data.value = res.data.data
pagination.value.itemCount = res.data.total
} catch (error) {
$message.error(error.msg)
}
loading.value = false
}
</script>
<style lang="scss" scoped></style>

20
src/views/user/route.js Normal file
View File

@@ -0,0 +1,20 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: '用户管理',
path: '/user',
component: Layout,
redirect: '/user_list',
children: [
{
name: 'Userlist',
path: 'user_list',
component: () => import('./index/index.vue'),
meta: {
title: '用户列表',
icon: 'mdi:account-multiple',
order: 10,
},
},
],
}

View File

@@ -8,7 +8,7 @@
<p text-16>Hello, {{ userStore.name }}</p>
<p mt-5 text-12 op-60>今天又是元气满满的一天</p>
</div>
<div ml-auto flex items-center>
<!-- <div ml-auto flex items-center>
<n-statistic label="待办" :value="4">
<template #suffix>/ 10</template>
</n-statistic>
@@ -22,11 +22,11 @@
<img alt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin" />
</a>
</n-statistic>
</div>
</div> -->
</div>
</n-card>
<n-card title="项目" size="small" :segmented="true" mt-15 rounded-10>
<!-- <n-card title="项目" size="small" :segmented="true" mt-15 rounded-10>
<template #header-extra>
<n-button text type="primary">更多</n-button>
</template>
@@ -46,7 +46,7 @@
<div h-0 w-300></div>
<div h-0 w-300></div>
</div>
</n-card>
</n-card> -->
</div>
</AppPage>
</template>