Merge branch 'fix/0707-ch'

* fix/0707-ch: (296 commits)
  clear
  修改
  list
  fix: test
  修改
  修改跳转问题
  fix:修改评分查询错误
  fix: bug修复
  fix:商家回复
  fix:商家回复
  fix:修改接口地址
  修改后管评论
  pref:去掉回复功能
  fix: 查询参数问题
  fix:详情页不显示
  fix: 切换更新详情
  fat:评价详情
  评价详情
  fix:修改生产配置
  fix:修改生产配置
  ...
main
ch 2 years ago
commit 0dd0ae0e36

@ -0,0 +1,18 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
// 新增、修复、文档、不影响逻辑的代码格式、重构、测试、回滚、编译、合并、优化、配置、其他
['feat', 'fix', 'doc', 'style', 'refactor', 'test', 'revert', 'build', 'merge', 'perf', 'conf', 'chore'],
],
'type-case': [0],
'type-empty': [0],
'scope-empty': [0],
'scope-case': [0],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never'],
'header-max-length': [0, 'always', 72],
},
};

@ -1,2 +1,5 @@
VITE_BASE_URL=/api
VITE_REQUEST_TIMEOUT=20000
VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
#VITE_SOCKET_URL=ws://192.168.10.93:8090/ws
VITE_REQUEST_TIMEOUT=5000
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn

@ -1,2 +0,0 @@
VITE_BASE_URL=https://gateway.mashibing.cn
VITE_REQUEST_TIMEOUT=20000

@ -1,2 +1,4 @@
VITE_BASE_URL=https://gateway.mashibing.com
VITE_BASE_URL=https://you-gateway.mashibing.com
VITE_SOCKET_URL=wss://you-gateway.mashibing.com/ws
VITE_REQUEST_TIMEOUT=20000
VITE_BROWSER_URL = https://you.mashibing.com

@ -1,2 +1,4 @@
VITE_BASE_URL=https://gateway-test.mashibing.cn
VITE_BASE_URL=https://k8s-horse-gateway.mashibing.cn/
VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
VITE_REQUEST_TIMEOUT=20000
VITE_BROWSER_URL = https://k8s-shop-pc.mashibing.cn

@ -0,0 +1,8 @@
src/assets
src/icons
public
dist
node_modules
src/utils/msb-im.js
src/utils/poto-req.js
src/utils/proto-rsp.js

@ -5,7 +5,8 @@ module.exports = {
node: true,
browser: true,
},
extends: ['eslint:recommended'],
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
plugins: ['prettier', 'vue'],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 'latest',
@ -16,10 +17,85 @@ module.exports = {
},
},
rules: {
'no-undef': 'error',
'no-alert': 'warn',
'no-debugger': 'warn',
'no-undef': 'error',
'no-else-return': 'error',
'no-console': 'off',
'vue/no-v-html': 'off',
'vue/html-self-closing': [
'warn',
{
html: {
void: 'any',
normal: 'any',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
'vue/multi-word-component-names': 'off',
'vue/order-in-components': [
'warn',
{
order: [
'el',
'name',
'key',
'parent',
'functional',
['delimiters', 'comments'],
['components', 'directives', 'filters'],
'extends',
'mixins',
['provide', 'inject'],
'ROUTER_GUARDS',
'layout',
'middleware',
'validate',
'scrollToTop',
'transition',
'loading',
'inheritAttrs',
'model',
['props', 'propsData'],
'emits',
'setup',
'fetch',
'asyncData',
'data',
'head',
'computed',
'watch',
'watchQuery',
'LIFECYCLE_HOOKS',
'methods',
['template', 'render'],
'renderError',
],
},
],
'vue/attributes-order': [
'warn',
{
order: [
'DEFINITION',
'LIST_RENDERING',
'CONDITIONALS',
'RENDER_MODIFIERS',
'GLOBAL',
'UNIQUE',
'TWO_WAY_BINDING',
'OTHER_DIRECTIVES',
'OTHER_ATTR',
'EVENTS',
'CONTENT',
],
alphabetical: true,
},
],
'prettier/prettier': 'error',
indent: [0, 4],
eqeqeq: [2, 'always'],
semi: [2, 'always'],
@ -30,5 +106,6 @@ module.exports = {
defineProps: true,
defineEmits: true,
defineExpose: true,
resolveDynamicComponent: true,
},
};

4
.gitignore vendored

@ -1,8 +1,8 @@
.idea
.DS_Store
.eslintcache
node_modules
dist
*.local
.history
src/.eslintrc.json
src/components.d.ts
src/auto-imports.d.ts

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit $1

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

@ -0,0 +1,3 @@
module.exports = {
'*.{jsx,js,vue,tsx,ts}': ['eslint --cache --fix --max-warnings=0'],
};

@ -1,3 +1,14 @@
{
"recommendations": ["johnsoncodehk.volar", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
"recommendations": [
"ms-ceintl.vscode-language-pack-zh-hans",
"eamodio.gitlens",
"formulahendry.auto-close-tag",
"formulahendry.auto-rename-tag",
"ecmel.vscode-html-css",
"abusaidm.html-snippets",
"octref.vetur",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"Vue.volar"
]
}

@ -40,5 +40,17 @@
"<style lang=\"less\" scoped></style>"
],
"description": "快速二次封装ElementPlus组件"
},
"try catch": {
"scope": "javascript,typescript",
"prefix": "trycatch",
"body": ["try {", "\t$0", "} catch (e) {", "\tconsole.info('取消$1', e);", "}"],
"description": "TryCatch代码块"
},
"proxy": {
"scope": "javascript,typescript",
"prefix": "proxy",
"body": ["const { proxy } = getCurrentInstance();"],
"description": "proxy对象"
}
}

@ -0,0 +1,37 @@
{
"nuxt.isNuxtApp": false,
"js/ts.implicitProjectConfig.checkJs": true,
"js/ts.implicitProjectConfig.strictNullChecks": true,
"git.autofetch": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"typescript.updateImportsOnFileMove.enabled": "always",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsx]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"vetur.format.defaultFormatter.html": "prettier",
"eslint.enable": true,
"eslint.format.enable": true,
"eslint.lintTask.enable": true,
"eslint.run": "onType",
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "html", "vue"]
}

@ -1,3 +1,4 @@
FROM nginx
COPY dist /usr/share/nginx/html
EXPOSE 80
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

@ -1,3 +1,312 @@
# shop-admin
# 马士兵管理平台模板
马士兵严选后台管理
## 技术架构
- vite
> 新兴主流项目编译构建工具,与 webpack 相比构建速度极快
- vue3
> vue 最新版本,声明式 API组件内各功能点代码耦合性低易扩展维护
- vue-router
> SPA 路由系统
- vuex
> 数据状态管理,规范组件之间数据传递,更好得监测数据状态变化
- axios
> AJAX 请求封装库,用于调用 API 接口
- element-plus
> 基础组件库
## 插件
- jsx
> javascript & react 语法,封装组件时编码更加灵活方便,功能开发时不推荐使用
- svg-icon
> 根据自定义 svg 文件生成 icon
- auto-import
> 编译时自动引入依赖
- components
> 编译时自动局部注册 ElementPlus 组件和 src/components 目录下的自定义组件
- style-import
> 编译时按需引用 ElementPlus 样式文件
- global-style
> 编译时自动注入全局样式和变量
- remove-console
> 打包时自动移除 console 语句
- vuex-persistedstate
> vuex 数据持久化
- legacy
> 浏览器兼容处理
- eslint
> 不影响 HMR 编译速度的情况下异步进行 eslint 代码校验
- husky
> git hooks 管理
- lint-staged
> git commit 时仅对提交部分代码进行 eslint 校验
- commitlint
> git commit 时对提交信息进行规范校验
## 规范约束
- 使用 VS Code 进行开发,并安装项目推荐的插件
- 不允许修改 src 目录以外的(项目配置)文件
- 有需要优化解决的问题记录到 TODO.md 文件中,包括技术方面的踩坑记录和业务方面的 BUG 记录
- 按照当前项目结构进行功能开发,不允许随意改变项目文件结构
## 项目结构
| SRC 目录 | 用途描述 |
| ---------- | ---------------------------- |
| api | API 接口定义 |
| assets | 资源文件,按功能模块分文件夹 |
| components | 自定义通用组件 |
| configs | 项目框架配置 |
| icons | 图标库svg 图标文件 |
| layouts | 页面框架布局 |
| plugins | 全局插件模块 |
| router | 本地路由模块 |
| store | 数据状态模块 |
| styles | 全局样式模块 |
| utils | 工具类库模块 |
| views | SFC 模块,页面文件 |
## 开发须知
### 通用组件
- 文件规范命名,使用简单易懂的单词概括组件的功能
- 写好 Props 参数注释
- 具有良好的扩展性和通用性
- 优化、修复问题时考虑向下兼容,尽可能不影响已有代码
- 使用 setup script 语法糖和 jsx 语法
- 有数据绑定功能的必须支持 v-mdoel 语法
### 路由页面
- 严格遵循既定的项目模块结构
- 单个功能页面之间通用组件应单独在页面目录创建 components 文件夹,而不是全部都放进 src/components
- 必须使用 defineComponent 包裹组件定义,并定义好具有唯一性的 name
- 不允许使用 this严禁滥用 getCurrentInstance()proxy 对象只用于访问全局方法
- 规范使用 ref、reactive、toRefs、unref 等响应式 API访问响应式数据时应使用 unref 包裹而不是访问 .value
- 各个功能点应区分代码块并写好注释,不允许延续 vue2 编码思想将各个功能点代码耦合在一起,特别复杂逻辑应代码分块并单独创建文件引入使用
- 不允许以任何形式直接修改 props 传入的值,包括对象中的属性和数组中的元素,如需双向绑定应使用触发事件 update:propsName 的方式
- 页面根元素应包含 \*-container 的 class无特殊情况使用单根元素多根元素会影响动画过渡效果
### 工具类库
- 项目已集成 lodash、dayjs不要重复造轮子
- 全局方法应在 plugins/global-api.js 中注册,而不是写成工具类到处引用
- 注释中应有完善的功能说明、入参出参说明、注意事项等
- 新增工具类后应及时通知所有项目开发者知悉
- 优化修复工具类应向下兼容确保不会影响已有代码,改动较大应召集相关人员商讨解决方案
- 应进行完善的逻辑测试和性能测试,确保不会影响项目稳定性,谨慎使用 setTimeout、setInterval 等循环\递归操作
### 状态管理
- 每个功能模块都要对应一个数据状态管理模块
- 必须通过 mutations 修改 state 中的数据
- 字典数据应全部存放在 state 中统一管理
- 接口调用及后续数据逻辑处理应在 actions 中进行,保证多处调用时行为统一
### 样式相关
- 参考 src/styles/gobalVariables.module.less颜色、字号、边距、圆角尽可能复用已有的变量
- 没有特殊需求不允许写全局样式stlye 标签必须加 scoped
- 深度选择器使用 :deep(&lt;selector&gt;){} 语法,>>> 、/deep/、v-deep:都已弃用
- 没有特殊需求不允许定义或使用 ID 选择器、属性选择器
### 分支管理
- 分支命名:父分支-功能描述-创建日期-创建人例如beta-userManagement-0323-xwk
- 紧急修复应基于 dev 分支创建,功能分支应基于 beta 分支创建
- 提测前先将 beta 分支合并到功能分支解决冲突,再将功能分支合并到 test 分支,严禁将 test 分支合并进其他任何分支
- 测试通过后先将 beta 分支合并到功能分支解决冲突,再通过提 push request 通知负责人进行代码审查,审查通过后由负责人合并代码
- 预发通过后先将 dev 分支合并到 beta 分支解决冲突,再将 beta 分支合并到 dev 分支,此操作只能由负责人进行
- 解决冲突时如涉及其他开发者代码,应跟当事人确认无误后再合并
### 提交代码
- 提交前确认项目能正常运行,改动部分功能正常
- 严格遵循 eslint、prettier 代码规范
- 严禁使用 git commit --no-verify -m "xxx" 强制提交代码
- 规范提交信息
| 前缀 | 使用场景 | 示例 |
| -------- | ------------------------------ | -------------------------- |
| feat | 新增功能点、模块 | feat: 用户管理 |
| fix | 修复 BUG | fix: 用户管理分页异常 |
| doc | 文档、注释更新 | doc: README |
| style | 样式、不影响逻辑的代码格式更新 | style: 用户管理标题字号 |
| refactor | 重构功能点、模块 | refactor: 路由模块逻辑重构 |
| test | 测试 | test: 测试工具类 |
| revert | 撤回提交 | revert: 有文件漏提交 |
| build | 编译打包 | build: 编译用户管理 |
| merge | 合并分支 | merge: beta into dev |
| perf | 性能、体验、逻辑优化 | pref: 路由模块解析性能 |
| conf | 配置更新 | conf: 项目 base 路径 |
| chore | 其他 | chore: 其他 |
## 全局方法
> 使用示例
```javascript
import { copy } from '@/plugins/global-api.js';
copy('hello world');
```
```javascript
const { proxy } = getCurrentInstance();
proxy.$copy('hello world');
```
| 名称 | 功能介绍 |
| -------- | --------------------- |
| copy | 复制文本 |
| download | 下载指定地址文件 |
| excel | 导出 excel 文件并下载 |
| dict | 字典查询 |
| name | 昵称和姓名拼接显示 |
## 全局组件
| 名称 | 功能介绍 |
| ------------- | ------------ |
| ElEditor | 富文本编辑器 |
| ElUploadImage | 图片上传 |
| TableList | 通用列表组件 |
## 心得总结
### prettier
> 统一代码格式
安装依赖
```
npm install --save-dev prettier
```
安装 VS Code 插件 prettier
> 新建工作区设置文件 .vscode/settings.json
> 设置保存代码时自动格式化
> 自动解决 eslint 代码格式问题
> 设置个文件类型默认格式化工具
```
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsx]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"vetur.format.defaultFormatter.html": "prettier",
}
```
### commitlint
> 提供 commit message 校验功能
安装依赖
```
npm install --save-dev @commitlint/config-conventional @commitlint/cli
```
编写配置
> 在项目根目录下创建 commitlint.config.js 或者 .commitlintrc.js
> 并在其中定义好可以使用前缀,如果格式不符将报错并导致提交失败
```
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'doc', 'style', 'refactor', 'test', 'revert', 'build', 'merge', 'perf', 'conf', 'chore'],
],
'type-case': [0],
'type-empty': [0],
'scope-empty': [0],
'scope-case': [0],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never'],
'header-max-length': [0, 'always', 72],
},
};
```
提交代码
```
git commit -m "feat: xxx"
```
> 注意要使用英文冒号,冒号后面跟空格
### HUSKY
> 可以对 git hooks 进行管理
安装依赖
```
npm install -D husky
```
在 package.json 中添加脚本
```
{
//...
"scripts": {
//...
"prepare": "husky install"
}
//...
}
```
> prepare 脚本会在 npm install不带参数之后自动执行。也就是说当我们执行 npm install 安装完项目依赖后会执行 husky install 命令,该命令会创建.husky/目录并指定该目录为 git hooks 所在的目录。
添加 git hooks
```
npx husky add .husky/pre-commit "npx lint-staged"
```
> 运行完该命令后我们会看到 .husky/ 目录下新增了一个名为 pre-commit 的 shell 脚本。
> 也就是说在在执行 git commit 命令时会先执行 pre-commit 这个脚本。
> 这个脚本的功能是运行 lint-staged 检查待提交的代码规范约束。
> 不能直接写 lint-staged会报错找不到命令npm run lint-staged 也可以不过需要在 package.json 中添加 lint-staged 这个命令,所以使用 npx lint-staged。
```
npx husky add .husky/commit-msg 'npx commitlint --edit "$1"'
```
> 运行完该命令后我们会看到 .husky/ 目录下新增了一个名为 commit-msg 的 shell 脚本。
> 也就是说在在执行 git commit 命令时会执行 commit-msg 这个脚本。
> 这个脚本的功能是运行 commitlint 检查 git commit 的 message 格式规范。
> 同 lint-staged 需要使用 npx 来运行命令,$1 代表开发者提交代码时输入的 commit message老版本husky中使用$HUSKY_GIT_PARAMS 来表示,已弃用

@ -0,0 +1,52 @@
# 待解决
## 严格模式下的 vuex 与表单 v-model 双向绑定冲突问题,每个属性单独用 copmputed 定义 getter、setter 可以解决但是太繁琐,目前是在页面上单独定义对象双向绑定监听变化存入 vuex
> xwk 2022.4.7
# 踩坑日记
## perttier 格式化失效
> xwk 2022.4.6
> 详情见 https://github.com/prettier/prettier-vscode/issues/2466
> 禁用 prettier 插件重新启用\安装
## 多个路由引用同一个 vue 页面时,打开这几个路由后就会有多个 TAB 页,切换 TAB 页时因为这几个路由引用的其实是同一个组件,所以初始数据会保持一致。比如新增页面应该是没有数据的,编辑数据需要回显,但是先打开新增页面再打开编辑页面,新增页面就会同步为编辑页面。
> xwk 2022.4.1
> 一般都是通过监听路由参数变化初始化数据,同时判断路由名称分别处理即可解决
## perttier 保存时不会自动格式化属性排序、需要执行命令才能格式化
> xwk 2022.3.23
> 在 .vscode/settings.json 配置
```json
{
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
```
## [Vue Router warn]: Unexpected error when starting the router: SyntaxError: Unexpected token '<'
> xwk 2022.3.26
> script 中使用了 jsx 语法但是 lang 没有定义为 jsx 或者 tsx
## [Vue warn]: Maximum recursive updates exceeded. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.
> xwk 0328
> el-tabs 使用 label 插槽,应该是 ElementPlus 的 BUG[Github 已有 Issues](https://github.com/element-plus/element-plus/issues/6839)
> ElementPlus 已在 2.1.7 版本修复了这个 BUG
## Uncaught (in promise) TypeError: Cannot read properties of null (reading 'shapeFlag')
> xwk 0330
> 对 props 使用对象展开符可能会展开 null
## 偶现 SVG 图标颜色渲染异常
> xwk 0328
> 可能是 vite-plugin-svg-icons 插件导致的问题,改用 vite-svg-loader 插件

@ -1,58 +1,54 @@
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
app: $IMAGES
name: $IMAGES
namespace: yanxuan
spec:
progressDeadlineSeconds: 600
replicas: 1
selector:
matchLabels:
app: $IMAGES
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
template:
metadata:
labels:
labels:
app: $IMAGES
spec:
imagePullSecrets:
- name: aliyun-docker-hub
containers:
- image: '$REGISTRY/$DOCKERHUB_NAMESPACE/$IMAGES:$BUILD_NUMBER'
name: app
ports:
- containerPort: $JAR_PORD
protocol: TCP
resources:
limits:
cpu: '0.5'
memory: 500Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
terminationGracePeriodSeconds: 30
name: $IMAGES
namespace: yanxuan
spec:
progressDeadlineSeconds: 600
replicas: 1
selector:
matchLabels:
app: $IMAGES
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
template:
metadata:
labels:
app: $IMAGES
spec:
imagePullSecrets:
- name: aliyun-docker-hub
containers:
- image: '$REGISTRY/$DOCKERHUB_NAMESPACE/$IMAGES:$BUILD_NUMBER'
name: app
ports:
- containerPort: $JAR_PORD
protocol: TCP
resources:
limits:
cpu: '0.5'
memory: 500Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
terminationGracePeriodSeconds: 30
---
kind: Service
apiVersion: v1
metadata:
labels:
app: $IMAGES
name: $IMAGES
namespace: yanxuan
name: $IMAGES
namespace: yanxuan
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: $IMAGES
sessionAffinity: None
type: ClusterIP
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: $IMAGES
type: ClusterIP

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<link href="/src/styles/loading.less" rel="stylesheet" />
<title>Vite App</title>
<title>马士兵严选管理平台</title>
</head>
<body>
<div id="app">

@ -7,7 +7,8 @@
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"jsx": "preserve"
},
"exclude": ["node_modules"]
}

8437
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,43 +1,66 @@
{
"name": "msb-shop-admin",
"author": {
"name": "向文可",
"email": "1041367524@qq.com"
},
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build:test": "vite build --mode test",
"build:preview": "vite build --mode preview",
"build:prod": "vite build --mode prod",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons": "^0.0.11",
"axios": "^0.26.1",
"dayjs": "^1.11.0",
"element-plus": "^2.1.2",
"lodash": "^4.17.21",
"qs": "^6.10.3",
"sortablejs": "^1.14.0",
"vue": "^3.2.25",
"vue-router": "^4.0.14",
"vuex": "^4.0.2"
},
"devDependencies": {
"@originjs/vite-plugin-global-style": "^1.0.2",
"@types/node": "^17.0.21",
"@vitejs/plugin-legacy": "^1.7.1",
"@vitejs/plugin-vue": "^2.2.0",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"consola": "^2.15.3",
"less": "^4.1.2",
"unplugin-auto-import": "^0.6.4",
"unplugin-vue-components": "^0.18.0",
"vite": "^2.8.0",
"vite-plugin-remove-console": "^0.0.6",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "^2.0.1"
}
"name": "msb-shop-admin",
"author": {
"name": "向文可",
"email": "1041367524@qq.com"
},
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build:test": "vite build --mode test",
"build:beta": "vite build --mode beta",
"build:prod": "vite build --mode prod",
"preview": "vite preview",
"prepare": "husky install",
"lint": "eslint src/**/*.{vue,js,jsx} --fix"
},
"dependencies": {
"@element-plus/icons": "^0.0.11",
"@vueup/vue-quill": "^1.0.0-beta.8",
"axios": "^0.26.1",
"china-area-data": "^5.0.1",
"dayjs": "^1.11.0",
"echarts": "^5.3.2",
"element-plus": "2.1.7",
"lodash": "^4.17.21",
"qs": "^6.10.3",
"quill-image-uploader": "^1.2.2",
"sortablejs": "^1.14.0",
"vue": "3.2.25",
"vue-router": "^4.0.14",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@commitlint/cli": "^13.2.1",
"@commitlint/config-conventional": "^13.2.0",
"@nabla/vite-plugin-eslint": "^1.4.0",
"@originjs/vite-plugin-global-style": "^1.0.2",
"@types/node": "^17.0.21",
"@vitejs/plugin-legacy": "^1.7.1",
"@vitejs/plugin-vue": "^2.2.0",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"airbnb": "^0.0.2",
"consola": "^2.15.3",
"eslint": "^8.11.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.5.0",
"husky": "^7.0.4",
"less": "^4.1.2",
"lint-staged": "^12.3.7",
"prettier": "^2.6.0",
"unplugin-auto-import": "^0.6.4",
"unplugin-vue-components": "^0.18.0",
"vite": "^2.8.0",
"vite-plugin-remove-console": "^0.0.6",
"vite-svg-loader": "^3.1.2"
},
"lint-staged": {
"src/**/*.{jsx,tsx,ts,js,vue}": [
"prettier --write",
"eslint --fix"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -1,10 +1,10 @@
<template>
<el-config-provider
:button="config.button"
:locale="config.locale"
:message="config.message"
:size="config.size"
:z-index="config.zIndex"
:button="config.button"
:message="config.message"
>
<router-view />
</el-config-provider>
@ -12,10 +12,9 @@
<script setup>
import zh from 'element-plus/lib/locale/lang/zh-cn';
const route = useRoute();
const config = reactive({
locale: zh,
size: 'default',
size: '',
zIndex: 300,
button: {
autoInsertSpace: true,

@ -11,7 +11,7 @@ export function sendSmsCode(params) {
// 登录
export async function login(data) {
return request({
url: '/uaa/sso/appManageLogin',
url: '/uc/user/loginByPasswordToB',
method: 'post',
data,
});
@ -19,14 +19,14 @@ export async function login(data) {
// 获取用户信息
export function getUserInfo() {
return request({
url: '/uc/user/v1/info/token',
url: '/uc/user/current/employee',
method: 'get',
});
}
// 获取权限列表
export function getPermission(params) {
return request({
url: '/u-admin/uc/ucPermission/listUserMenu',
url: '/uc/menu',
method: 'get',
params,
});

@ -0,0 +1,86 @@
/*
* @Author: xwk
* @Date: 2022-05-24 17:00:26
* @LastEditors: ch
* @LastEditTime: 2022-06-13 14:26:53
* @Description: file content
*/
import request from '@/utils/request.js';
export const login = (params) => {
return request({
url: '/im/waiter/getWaiterConnectTicket',
method: 'get',
params,
});
};
export const searchService = (id) => {
return request({
url: '/im/admin/waiter/findAllWaiter/' + id,
method: 'get',
});
};
export const searchSession = (params) => {
return request({
url: '/im/admin/waiter/findWaiterSessions',
method: 'get',
params,
});
};
export const searchMessage = (params) => {
return request({
url: '/im/admin/waiter/findSessionMessage',
method: 'get',
params,
});
};
export const searchSummary = (params) => {
return request({
url: '/im/admin/waiter/waiterStatistics',
method: 'get',
params,
});
};
/**
* 获取链接凭证
*/
export const getCustomeServiceTicket = () => {
return request({
url: '/mall/im/admin/ticket',
method: 'get',
params: {
ticketType: 'CONNECT_TICKET',
},
});
};
/**
* 获取当前客服
*/
export const getCustomerService = () => {
return request({
url: '/mall/im/admin/waiterUser/getWaiterByUserId',
method: 'get',
});
};
/**
* 获取可转移客服列表
* @param {*} params
*/
export const customerServiceList = (params) => {
return request({
url: '/mall/im/admin/waiter',
method: 'get',
params,
});
};
/**
* 转移客服
* @param {*} params
*/
export const transferCustomerService = (data) => {
return request({
url: '/mall/im/admin/waiter/transfer',
method: 'post',
data,
});
};

@ -0,0 +1,36 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/im/admin/waiterUser',
method: 'get',
params,
});
};
export const detail = (userId) => {
return request({
url: '/mall/im/admin/waiterUser/getWaiterByUserId',
method: 'get',
params: { userId },
});
};
export const create = (data) => {
return request({
url: '/mall/im/admin/waiterUser',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/mall/im/admin/waiterUser',
method: 'put',
data,
});
};
export const remove = (ids) => {
return request({
url: '/mall/im/admin/waiterUser',
method: 'delete',
data: { ids },
});
};

@ -0,0 +1,14 @@
import request from '@/utils/request.js';
export const search = () => {
return request({
url: '/mall/base/orderConfig/getOrderConfig',
method: 'get',
});
};
export const update = (data) => {
return request({
url: '/mall/base/orderConfig/saveOrUpdate',
method: 'post',
data,
});
};

@ -0,0 +1,45 @@
import request from '@/utils/request';
// OSS签名
export function sign(serviceName, configId) {
return request({
url: '/oss/oss/generateOssSignature',
method: 'POST',
data: {
serviceName,
configId,
},
});
// console.info(serviceName, configId);
// return {
// accessId: 'LTAI4GHRNb5Xn2w5NeHVbR4c',
// policy: 'eyJleHBpcmF0aW9uIjoiMjAyMi0wNC0xNVQyMDowODoyNi4zMTlaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJ0ZXN0LyJdXX0=',
// signature: 'okaB3sNp3vzyfM0S3ypudaUAZ+0=',
// dir: 'test/',
// host: 'https://msb-edu-dev.oss-cn-beijing.aliyuncs.com',
// expire: '1650053306',
// };
}
// 上传文件
export async function upload(serviceName, configId, file, cancelToken) {
let oss = await sign(serviceName, configId);
let data = new FormData();
let arr = file.name.split('/');
arr[arr.length - 1] = encodeURIComponent(arr[arr.length - 1]);
data.append('name', arr.join('/'));
data.append('key', `${oss.dir}${'${filename}'}`);
data.append('policy', oss.policy);
data.append('OSSAccessKeyId', oss.accessId);
data.append('Signature', oss.signature);
data.append('success_action_status', 200);
data.append('file', file);
return request({
url: oss.host,
method: 'POST',
data,
headers: {
oss: true,
},
timeout: 0,
cancelToken,
});
}

@ -0,0 +1,36 @@
/*
* @Author: ch
* @Date: 2022-06-15 17:55:43
* @LastEditors: ch
* @LastEditTime: 2022-06-24 09:44:44
* @Description: file content
*/
import request from '@/utils/request.js';
//获取评论列表
export const commentList = (params) =>
request({
url: '/mall/comment/admin/comment',
method: 'get',
params,
});
// 获取评价详情
export const commentDetail = ({ id }) =>
request({
url: `/mall/comment/admin/comment/getCommentDetail/${id}`,
method: 'get',
});
//评论
export const commentAdd = (data) =>
request({
url: '/mall/comment/admin/merchantComment',
method: 'post',
data,
});
// 更新评价显示状态
export const updateCommentShow = (data) =>
request({
url: '/mall/comment/admin/comment',
method: 'put',
data,
});

@ -0,0 +1,99 @@
import request from '@/utils/request.js';
// 获取头部数据 今日访客 今日订单 今日销售额 近七天销售额
export const summary = () => {
return request({
url: '/mall/base/frontPage/getHeaderData',
method: 'get',
});
};
// 获取待处理事务数据
export const order = () => {
return request({
url: '/mall/base/frontPage/getWaitHandleAffairs',
method: 'get',
});
};
// 获取商品总览数据
export const product = () => {
return request({
url: '/mall/base/frontPage/getProductOverview',
method: 'get',
});
};
// 获取用户总览数据
export const customer = () => {
return request({
url: '/mall/base/frontPage/getUserOverviewData',
method: 'get',
});
};
// 获取订单数量同比增长数据
export const orderInfo = () => {
return request({
url: '/mall/base/frontPage/getOrderTotal',
method: 'get',
});
};
// 获取订单统计数据
export const orderSummary = (params) => {
return request({
url: '/mall/base/frontPage/listOrderStatistics',
method: 'get',
params,
});
};
// 获取今日订单统计数据
export const orderToday = () => {
return request({
url: '/mall/base/frontPage/listTodayOrderStatistics',
method: 'get',
});
};
// 获取访客数量同比增长数据
export const customerInfo = () => {
return request({
url: '/mall/base/frontPage/getVisitorTotal',
method: 'get',
});
};
// 获取访客统计数据
export const customerSummary = (params) => {
return request({
url: '/mall/base/frontPage/listVisitorStatistics',
method: 'get',
params,
});
};
// 获取今日访客统计数据
export const customerToday = () => {
return request({
url: '/mall/base/frontPage/listTodayVisitorStatistics',
method: 'get',
});
};
// 获取销售额同比增长数据
export const moneyInfo = () => {
return request({
url: '/mall/base/frontPage/getSalesTotal',
method: 'get',
});
};
// 获取销售额统计数据
export const moneySummary = (params) => {
return request({
url: '/mall/base/frontPage/listSalesStatistics',
method: 'get',
params,
});
};
// 获取今日销售额统计数据
export const moneyToday = () => {
return request({
url: '/mall/base/frontPage/listTodaySalesStatistics',
method: 'get',
});
};

@ -0,0 +1,106 @@
import request from '@/utils/request.js';
export const online = (params) => {
return request({
url: '/im/admin/count/online',
method: 'get',
params,
});
};
export const hours = (params) => {
return request({
url: '/im/admin/count/hoursMessage',
method: 'get',
params,
});
};
export const days = (params) => {
return request({
url: '/im/admin/count/daysMessage',
method: 'get',
params,
});
};
export const searchSystem = (params) => {
return request({
url: '/im/admin/thirdSystem',
method: 'get',
params,
});
};
export const createSystem = (data) => {
return request({
url: '/im/admin/thirdSystem',
method: 'post',
data,
});
};
export const updateSystem = (data) => {
return request({
url: '/im/admin/thirdSystem',
method: 'put',
data,
});
};
export const removeSystem = (data) => {
return request({
url: '/im/admin/thirdSystem',
method: 'delete',
data,
});
};
export const searchStore = (params) => {
return request({
url: '/im/admin/storeConfig',
method: 'get',
params,
});
};
export const createStore = (data) => {
return request({
url: '/im/admin/storeConfig',
method: 'post',
data,
});
};
export const updateStore = (data) => {
return request({
url: '/im/admin/storeConfig',
method: 'put',
data,
});
};
export const removeStore = (data) => {
return request({
url: '/im/admin/storeConfig',
method: 'delete',
data,
});
};
export const searchWaiter = (params) => {
return request({
url: '/im/admin/waiter',
method: 'get',
params,
});
};
export const createWaiter = (data) => {
return request({
url: '/im/admin/waiter',
method: 'post',
data,
});
};
export const updateWaiter = (data) => {
return request({
url: '/im/admin/waiter',
method: 'put',
data,
});
};
export const removeWaiter = (data) => {
return request({
url: '/im/admin/waiter',
method: 'delete',
data,
});
};

@ -0,0 +1,48 @@
/*
* @Author: ch
* @Date: 2022-06-01 11:03:18
* @LastEditors: ch
* @LastEditTime: 2022-06-01 14:16:14
* @Description: file content
*/
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/marketing/advertisement',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/mall/marketing/advertisement/' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/mall/marketing/advertisement',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/mall/marketing/advertisement/' + data.id,
method: 'put',
data,
});
};
export const sort = (data) => {
return request({
url: '/mall/marketing/advertisement/updateSort',
method: 'put',
data,
});
};
export const remove = (id) => {
return request({
url: `/mall/marketing/advertisement?id=${id}`,
method: 'delete',
});
};

@ -0,0 +1,42 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/marketing/activity',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/mall/marketing/activity/' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/mall/marketing/activity',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/mall/marketing/activity/' + data.id,
method: 'put',
data,
});
};
export const online = (params) => {
return request({
url: '/mall/marketing/activity/online/' + params.id,
method: 'put',
params,
});
};
export const remove = (idList) => {
return request({
url: '/mall/marketing/activity',
method: 'delete',
params: { idList },
});
};

@ -0,0 +1,38 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/marketing/activityProduct',
method: 'get',
params,
});
};
export const create = (data) => {
return request({
url: '/mall/marketing/activityProduct',
method: 'post',
data,
});
};
export const remove = (idList) => {
return request({
url: '/mall/marketing/activityProduct',
method: 'delete',
params: { idList },
});
};
export const searchSkus = (activityProductId) => {
return request({
url: '/mall/marketing/activityProductSku',
method: 'get',
params: { activityProductId },
});
};
export const saveSkus = (data) => {
return request({
url: '/mall/marketing/activityProductSku',
method: 'post',
data,
});
};

@ -0,0 +1,36 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/marketing/activityTime',
method: 'get',
params,
});
};
export const create = (data) => {
return request({
url: '/mall/marketing/activityTime',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/mall/marketing/activityTime/' + data.id,
method: 'put',
data,
});
};
export const enable = (params) => {
return request({
url: '/mall/marketing/activityTime/enable/' + params.id,
method: 'put',
params,
});
};
export const remove = (idList) => {
return request({
url: '/mall/marketing/activityTime',
method: 'delete',
params: { idList },
});
};

@ -0,0 +1,29 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/marketing/productRecommended',
method: 'get',
params,
});
};
export const create = (data) => {
return request({
url: '/mall/marketing/productRecommended',
method: 'post',
data,
});
};
export const enable = (data) => {
return request({
url: '/mall/marketing/productRecommended',
method: 'put',
data,
});
};
export const remove = (idList) => {
return request({
url: '/mall/marketing/productRecommended',
method: 'delete',
params: { idList },
});
};

@ -0,0 +1,77 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/uc/department/tree',
method: 'get',
params,
});
};
export const searchEmployee = (params) => {
return request({
url: '/uc/employee/department',
method: 'get',
params,
});
};
export const transferEmployee = (data) => {
return request({
url: '/uc/employee/employee',
method: 'put',
params: data,
});
};
export const children = (params) => {
return request({
url: '/uc/department/child',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/uc/department/' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/uc/department',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/uc/department/' + data.id,
method: 'put',
data,
});
};
export const remove = (idList) => {
return request({
url: '/uc/department',
method: 'delete',
params: { idList },
});
};
export const searchRole = (params) => {
return request({
url: '/uc/role/department',
method: 'get',
params,
});
};
export const addRole = (data) => {
return request({
url: '/uc/department/role',
method: 'put',
data,
});
};
export const delRole = (data) => {
return request({
url: '/uc/department/role',
method: 'delete',
data,
});
};

@ -0,0 +1,62 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/uc/employee',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/uc/employee/' + id,
method: 'get',
});
};
export const searchPermission = (id) => {
return request({
url: '/uc/employee/role/' + id,
method: 'get',
});
};
export const savePermission = (id, data) => {
return request({
url: '/uc/employee/role/' + id,
method: 'put',
data,
});
};
export const create = (data) => {
return request({
url: '/uc/employee',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/uc/employee',
method: 'put',
data,
});
};
export const remove = (idList) => {
return request({
url: '/uc/employee',
method: 'delete',
params: { idList },
});
};
export const enable = (params) => {
return request({
url: '/uc/employee/enable',
method: 'put',
params,
});
};
export const reset = (employeeId) => {
return request({
url: '/uc/employee/password',
method: 'put',
params: { employeeId },
});
};

@ -0,0 +1,42 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/uc/permission',
method: 'get',
params,
});
};
export const searchFree = () => {
return request({
url: '/uc/permission/NotDistribution',
method: 'get',
});
};
export const assign = (params) => {
return request({
url: '/uc/permission/menu',
method: 'put',
params,
});
};
export const create = (data) => {
return request({
url: '/uc/permission',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/uc/permission/' + data.id,
method: 'put',
data,
});
};
export const remove = (idList) => {
return request({
url: '/uc/permission',
method: 'delete',
params: { idList },
});
};

@ -0,0 +1,29 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/uc/menu/tree',
method: 'get',
params,
});
};
export const create = (data) => {
return request({
url: '/uc/menu',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/uc/menu/' + data.id,
method: 'put',
data,
});
};
export const remove = (idList) => {
return request({
url: '/uc/menu',
method: 'delete',
params: { idList },
});
};

@ -0,0 +1,42 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/uc/role',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/uc/role/' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/uc/role',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/uc/role/' + data.id,
method: 'put',
data,
});
};
export const updateMenu = (data) => {
return request({
url: '/uc/role/menu',
method: 'put',
data,
});
};
export const remove = (idList) => {
return request({
url: '/uc/role',
method: 'delete',
params: { idList },
});
};

@ -0,0 +1,8 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/uc/system',
method: 'get',
params,
});
};

@ -0,0 +1,42 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/product/admin/productCategory',
method: 'get',
params,
});
};
export const create = (data) => {
return request({
url: '/mall/product/admin/productCategory',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/mall/product/admin/productCategory/' + data.id,
method: 'put',
data,
});
};
export const remove = (params) => {
return request({
url: '/mall/product/admin/productCategory',
method: 'delete',
params,
});
};
export const sort = (data) => {
return request({
url: '/mall/product/admin/productCategory/updateSort',
method: 'put',
data,
});
};
export const transform = (data) => {
return request({
url: `/mall/product/admin/productCategory/transfer/${data.sourceId}/${data.targetId}`,
method: 'put',
});
};

@ -0,0 +1,107 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/trade/admin/tradeOrder/page',
method: 'get',
params,
});
};
export const exportAll = (params) => {
return request({
url: '/mall/trade/admin/tradeOrder/page',
method: 'get',
params: {
...params,
export: true,
},
headers: {
download: true,
},
});
};
export const detail = (id) => {
return request({
url: '/mall/trade/admin/tradeOrder/' + id,
method: 'get',
});
};
export const shipList = (orderIds) => {
return request({
url: '/mall/trade/admin/tradeOrder/listDeliveryOrder',
method: 'get',
params: { orderIds },
});
};
export const logistics = (id) => {
return request({
url: '/mall/trade/admin/tradeOrder/logistics/' + id,
method: 'get',
});
};
export const summary = () => {
return request({
url: '/mall/trade/admin/tradeOrder/statistics',
method: 'get',
});
};
export const updateFees = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/updateAmount',
method: 'put',
data,
});
};
export const updateAddress = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/updateRecipient',
method: 'put',
data,
});
};
export const cancel = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/cancel',
method: 'put',
data,
});
};
export const close = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/close',
method: 'put',
data,
});
};
export const searchShip = () => {
return request({
url: '/mall/trade/admin/tradeOrder/logisticsCompany',
method: 'get',
});
};
export const send = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/delivery',
method: 'put',
data,
});
};
export const sendVirtual = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/virtualDelivery/' + data,
method: 'put',
});
};
export const sendVirtualAll = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/batchVirtualDelivery',
method: 'put',
data,
});
};
export const sendAll = (data) => {
return request({
url: '/mall/trade/admin/tradeOrder/batchDelivery',
method: 'put',
data,
});
};

@ -0,0 +1,126 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/product/admin/product/page',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/mall/product/admin/product/' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/mall/product/admin/product',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/mall/product/admin/product/' + data.id,
method: 'put',
data,
});
};
export const enable = (data) => {
return request({
url: '/mall/product/admin/product/enable/' + data.id,
method: 'put',
params: { isEnable: data.isEnable },
});
};
export const remove = (params) => {
return request({
url: '/mall/product/admin/product',
method: 'delete',
params,
});
};
export const searchSkus = (id) => {
return request({
url: '/mall/product/admin/productSku/list/' + id,
method: 'get',
});
};
export const createSkus = (id, data) => {
return request({
url: '/mall/product/admin/productSku/' + id,
method: 'post',
data,
});
};
export const createAndClearSkus = (id, data) => {
return request({
url: '/mall/product/admin/productSku/deleteAllAndAdd/' + id,
method: 'post',
data,
});
};
export const searchAttrs = (id) => {
return request({
url: '/mall/product/admin/productAttributeGroup/' + id,
method: 'get',
});
};
export const createAttrs = (data) => {
return request({
url: '/mall/product/admin/productAttributeGroup',
method: 'post',
data,
});
};
export const updateAttrs = (data) => {
return request({
url: '/mall/product/admin/productAttributeGroup/' + data.id,
method: 'put',
data,
});
};
export const updateAttrsSort = (data) => {
return request({
url: '/mall/product/admin/productAttributeGroup/updateSort',
method: 'put',
data,
});
};
export const removeAttrs = (params) => {
return request({
url: '/mall/product/admin/productAttributeGroup',
method: 'delete',
params,
});
};
export const searchAttrsValue = (id) => {
return request({
url: '/mall/product/admin/productAttribute/list/' + id,
method: 'get',
});
};
export const createAttrsValue = (data) => {
return request({
url: '/mall/product/admin/productAttribute',
method: 'post',
data,
});
};
export const updateAttrsValue = (data) => {
return request({
url: '/mall/product/admin/productAttribute/' + data.id,
method: 'put',
data,
});
};
export const removeAttrsValue = (params) => {
return request({
url: '/mall/product/admin/productAttribute',
method: 'delete',
params,
});
};

@ -0,0 +1,88 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/trade/admin/refundOrder/page',
method: 'get',
params,
});
};
export const exportAll = (params) => {
return request({
url: '/mall/trade/admin/refundOrder/page',
method: 'get',
params: {
...params,
export: true,
},
headers: {
download: true,
},
});
};
export const refundDetail = (id) => {
return request({
url: '/mall/trade/admin/refundOrder/refundInfo/' + id,
method: 'get',
});
};
export const returnDetail = (id) => {
return request({
url: '/mall/trade/admin/refundOrder/returnInfo/' + id,
method: 'get',
});
};
export const logistics = (id) => {
return request({
url: '/mall/trade/admin/refundOrder/logistics/' + id,
method: 'get',
});
};
export const summary = () => {
return request({
url: '/mall/trade/admin/refundOrder/statistics',
method: 'get',
});
};
export const resolveReceive = (data) => {
return request({
url: '/mall/trade/admin/refundOrder/agreeReceiving',
method: 'put',
data,
});
};
export const rejectReceive = (data) => {
return request({
url: '/mall/trade/admin/refundOrder/disagreeReceiving',
method: 'put',
data,
});
};
export const resolveRefund = (data) => {
return request({
url: '/mall/trade/admin/refundOrder/agreeRefund',
method: 'put',
data,
});
};
export const rejectRefund = (data) => {
return request({
url: '/mall/trade/admin/refundOrder/disagreeRefund',
method: 'put',
data,
});
};
export const resolveReturn = (data) => {
return request({
url: '/mall/trade/admin/refundOrder/agreeReturn',
method: 'put',
data,
});
};
export const rejectReturn = (data) => {
return request({
url: '/mall/trade/admin/refundOrder/disagreeReturn',
method: 'put',
data,
});
};

@ -0,0 +1,35 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/search/admin/searchConfig',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/search/admin/searchConfig/' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/search/admin/searchConfig',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/search/admin/searchConfig',
method: 'put',
data,
});
};
export const remove = (id) => {
return request({
url: '/search/admin/searchConfig',
method: 'delete',
params: { id },
});
};

@ -0,0 +1,35 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/search/admin/systemConfig',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/search/admin/systemConfig/' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/search/admin/systemConfig',
method: 'post',
data,
});
};
export const update = (data) => {
return request({
url: '/search/admin/systemConfig',
method: 'put',
data,
});
};
export const remove = (id) => {
return request({
url: '/search/admin/systemConfig',
method: 'delete',
params: { id },
});
};

@ -0,0 +1,15 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: 'uc/admin/user/page',
method: 'get',
params,
});
};
export const enable = (params) => {
return request({
url: 'uc/admin/user/enable',
method: 'put',
params,
});
};

@ -0,0 +1,28 @@
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/mall/marketing/appMessagePush',
method: 'get',
params,
});
};
export const detail = (id) => {
return request({
url: '/mall/marketing/appMessagePush' + id,
method: 'get',
});
};
export const create = (data) => {
return request({
url: '/mall/marketing/appMessagePush',
method: 'post',
data,
});
};
export const remove = (idList) => {
return request({
url: '/mall/marketing/appMessagePush',
method: 'delete',
params: { idList },
});
};

@ -1,40 +0,0 @@
const mock = (data) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, Math.random() * 1500 + 500);
});
export const findUserList = (data) => {
return mock({
content: [
{
id: 1,
username: 'user001',
nickname: '张三',
sex: 1,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: true,
},
{
id: 2,
username: 'user003',
nickname: '李四',
sex: 0,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: true,
},
{
id: 3,
username: 'user003',
nickname: '王五',
sex: 1,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: false,
},
],
totalElements: 3,
});
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

@ -0,0 +1,47 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import data from 'china-area-data';
import ElCascader from './extra/ElCascader.vue';
const props = defineProps({
props: {
type: Object,
default: () => {
return {
expandTrigger: 'hover',
};
},
},
info: {
type: Array,
default: () => [],
},
});
const attrs = useAttrs();
const slots = useSlots();
const emits = defineEmits(['update:info']);
const convert = (obj) =>
obj
? Object.entries(obj).map((entry) => {
return { label: entry[1], value: entry[0], children: convert(data[entry[0]]) };
})
: [];
const options = convert(data[86]);
const handleInfo = (code) => {
let province = data[86],
city = data[code[0]],
area = data[code[1]];
let res = [province?.[code[0]], city?.[code[1]], area?.[code[2]]];
return res;
};
watch(
() => attrs.modelValue,
(value) => {
emits('update:info', handleInfo(value));
},
{ immediate: true, deep: true }
);
const render = () => <ElCascader {...{ ...props, options }} {...attrs} v-slots={slots} />;
</script>
<style lang="less" scoped></style>

@ -0,0 +1,177 @@
<template>
<component :is="render" />
</template>
<script lang="jsx">
export default defineComponent({
inheritAttrs: false,
});
</script>
<script setup lang="jsx">
import { upload } from '@/api/file';
import { Quill, QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import ImageUploader from 'quill-image-uploader';
Quill.register('modules/imageUploader', ImageUploader);
const props = defineProps({
configId: {
type: String,
default: 'product',
},
serviceName: {
type: String,
default: 'mall-product',
},
readonly: {
type: Boolean,
default: false,
},
preview: {
type: [Boolean, String],
default: 'app',
},
});
const attrs = useAttrs();
const emits = defineEmits(['update:modelValue', 'change']);
const editor = ref(null);
const options = {
bounds: '.el-editor',
debug: 'error',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
['link', 'image'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ direction: 'rtl' }],
[{ size: ['small', false, 'large', 'huge'] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
['clean'],
],
imageUploader: {
upload: async (file) => {
let res = await upload(props.serviceName, props.configId, file);
if (!res) {
throw new Error('上传失败');
}
return res;
},
},
},
placeholder: '请输入内容...',
readOnly: props.readonly,
theme: 'snow',
};
const content = ref(null);
watch(
() => content,
(value, old) => {
emits('update:modelValue', unref(value));
emits('change', unref(value), unref(old));
},
{ deep: true }
);
const handleReady = () => {
unref(editor)?.setHTML(attrs.modelValue || '');
};
watch(
() => attrs.modelValue,
(value) => {
if (value !== unref(content)) {
handleReady();
}
},
{ immediate: true }
);
const handleUpdateContent = () => {
content.value = unref(editor).getHTML();
};
const preview = ref(props.preview === false ? false : typeof props.preview === 'string' ? props.preview : 'app');
const handlePreview = (mode) => {
preview.value = mode;
};
const render = () => (
<div class="el-editor">
<div class="editor">
<QuillEditor
ref={editor}
options={options}
{...attrs}
content={unref(content)}
on={{
'update:content': handleUpdateContent,
ready: handleReady,
}}
/>
</div>
{unref(preview) ? (
<div class={{ preview: true, ['--' + unref(preview)]: true, 'ql-snow': true }}>
<h3 class="header">
<div
class={{ btn: true, active: unref(preview) === 'app' }}
onClick={() => handlePreview('app')}
>
APP预览
</div>
<div class={{ btn: true, active: unref(preview) === 'pc' }} onClick={() => handlePreview('pc')}>
PC预览
</div>
</h3>
<div class="content ql-editor" v-html={unref(content)}></div>
</div>
) : (
''
)}
</div>
);
</script>
<style lang="less" scoped>
.el-editor {
width: 100%;
height: 480px;
display: flex;
:deep(.editor) {
display: flex;
flex-direction: column;
flex: 1;
}
:deep(.preview) {
height: 100%;
margin-left: 20px;
border: 1px solid #d1d5db;
&.--app {
width: 375px;
}
&.--pc {
width: 640px;
}
.header {
padding: 10px;
border-bottom: 1px solid #d1d5db;
display: flex;
.btn {
color: #d1d5db;
cursor: pointer;
&.active {
color: #000;
}
+ .btn {
margin-left: 10px;
}
}
}
.content {
padding: 10px;
}
}
}
</style>

@ -0,0 +1,162 @@
<template>
<div class="upload-box">
<el-upload
v-bind="props"
action="none"
:before-upload="handleBeforeUpload"
:class="{ max: imgList.length === props.limit }"
:file-list="imgList"
:http-request="handleUpload"
list-type="text"
:on-exceed="handleExceed"
:on-remove="handleRemove"
show-file-list
>
<el-button type="primary">
<el-icon name="Plus" style="top: 0" />
<span>上传文件</span>
</el-button>
</el-upload>
<div class="el-upload__tip">支持小于 {{ fmtSize }} 文件</div>
</div>
</template>
<script setup lang="jsx">
import { upload } from '@/api/file';
import { ElMessage } from '@/plugins/element-plus';
import 'element-plus/es/components/image/style/css';
const props = defineProps({
configId: {
type: String,
required: true,
},
serviceName: {
type: String,
default: 'mall-product',
},
drag: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
defualt: false,
},
disabled: {
type: Boolean,
defualt: false,
},
limit: {
type: Number,
default: 1,
},
size: {
type: Number,
default: 1024 * 1024 * 20,
},
accept: {
type: String,
default: '*/*',
},
loading: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue', 'update:loading']);
const imgList = ref([]);
const attrs = useAttrs();
watch(
() => attrs.modelValue,
(value) => {
value = value instanceof Array ? value : [value];
if (
unref(imgList)
.map((item) => item.url)
.join(',') !== value?.join(',')
) {
imgList.value = value
.filter((item) => item)
.map((item) => {
return {
name: item,
response: item,
url: item,
};
});
}
},
{ immediate: true, deep: true }
);
watch(
() => imgList,
() => {
const arr = unref(imgList).map((item) => item.response);
if (arr.every((item) => !!item)) {
const value = props.limit === 1 ? arr[0] : arr;
emits('update:modelValue', value);
}
},
{ deep: true }
);
const handleExceed = (list) => {
console.info('[upload] exceed', list);
ElMessage.error('超出最大上传数量');
};
const handleRemove = (file) => {
console.info('[upload] remove', file);
if (file.status !== 'success') {
unref(cancelToken).cancel('用户手动取消请求');
}
};
const handleBeforeUpload = (file) => {
console.info('[upload] upload', file);
let res = true;
if (file.size >= props.size) {
ElMessage.error('超出文件大小限制');
res = false;
}
return res;
};
const cancelToken = ref(null);
const loading = ref(props.loading);
watch(loading, (value) => {
emits('update:loading', value);
});
const handleUpload = async ({ file }) => {
loading.value = true;
cancelToken.value = axios.CancelToken.source();
let res = await upload(props.serviceName, props.configId, file, unref(cancelToken).token);
loading.value = false;
return res;
};
const fmtSize = computed(() => {
const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
let res = props.size,
unit = 0;
while (res >= 800) {
res /= 1024;
unit++;
}
return res + units[unit];
});
</script>
<style lang="less" scoped>
.upload-box {
:deep(.el-upload-list__item-name) {
max-width: calc(100% - 40px);
overflow: auto;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: #999;
}
&::-webkit-scrollbar-track {
border-radius: 10px;
background-color: #ccc;
}
}
}
</style>

@ -0,0 +1,246 @@
<template>
<div class="upload-box">
<div :class="{ 'upload-box__sortable': props.sortable }">
<ul v-if="props.sortable" ref="sortableRef" class="sortable">
<li v-for="(item, idx) in imgList" :key="item.uid" class="sortable--item">
<img :src="item.url" />
<span class="sortable--item-hover">
<el-icon class="sortable--item-icon" name="ZoomIn" @click="handlePreview(item)" />
<el-icon class="sortable--item-icon" name="Delete" @click="handleRemove(idx)" />
</span>
</li>
</ul>
<el-upload
v-bind="props"
action="none"
:before-upload="handleBeforeUpload"
:class="{ max: imgList.length === props.limit, 'sortable--submit': props.sortable }"
:file-list="imgList"
:http-request="handleUpload"
:list-type="!props.sortable ? 'picture-card' : 'picture'"
:on-exceed="handleExceed"
:on-preview="handlePreview"
:show-file-list="!props.sortable"
>
<el-icon name="Plus" />
</el-upload>
</div>
<el-image
v-if="preview"
ref="refsPreview"
alt="图片预览"
:src="preview"
style="position: absolute; z-index: -9999"
@close="preview = null"
@load="$event.path[0].click()"
/>
<div class="el-upload__tip">支持小于 {{ fmtSize }} 文件</div>
</div>
</template>
<script setup lang="jsx">
import { upload } from '@/api/file';
import { ElMessage } from '@/plugins/element-plus';
import 'element-plus/es/components/image/style/css';
import Sortable from 'sortablejs';
const props = defineProps({
configId: {
type: String,
required: true,
},
sortable: {
type: Boolean,
default: false,
},
serviceName: {
type: String,
default: 'mall-product',
},
drag: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
defualt: false,
},
disabled: {
type: Boolean,
defualt: false,
},
limit: {
type: Number,
default: 1,
},
size: {
type: Number,
default: 1024 * 1024 * 20,
},
accept: {
type: String,
default: 'image/*',
},
});
const emits = defineEmits(['update:modelValue']);
const imgList = ref([]);
const attrs = useAttrs();
watch(
() => attrs.modelValue,
(value) => {
value = value instanceof Array ? value : [value];
if (
unref(imgList)
.map((item) => item.url)
.join(',') !== value?.join(',')
) {
imgList.value = value
.filter((item) => item)
.map((item) => {
return {
name: item,
response: item,
url: item,
};
});
}
},
{ immediate: true, deep: true }
);
watch(
() => imgList,
() => {
const arr = unref(imgList).map((item) => item.response);
if (arr.every((item) => !!item)) {
const value = props.limit === 1 ? arr[0] : arr;
emits('update:modelValue', value);
}
},
{ deep: true }
);
const refsPreview = ref(null);
const preview = ref('');
const handlePreview = (file) => {
console.info('[upload] preview', file);
preview.value = file.url;
};
const handleRemove = (idx) => {
imgList.value.splice(idx, 1);
};
const handleExceed = (list) => {
console.info('[upload] exceed', list);
ElMessage.error('超出最大上传数量');
};
const handleBeforeUpload = (file) => {
console.info('[upload] upload', file);
let res = true;
if (file.type.startsWith('image/')) {
if (file.size >= props.size) {
ElMessage.error('超出文件大小限制');
res = false;
}
} else {
ElMessage.error('只允许上传图片');
res = false;
}
return res;
};
const handleUpload = async ({ file }) => {
return await upload(props.serviceName, props.configId, file);
};
const fmtSize = computed(() => {
const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
let res = props.size,
unit = 0;
while (res >= 800) {
res /= 1024;
unit++;
}
return res + units[unit];
});
const sortableRef = ref(null);
const sortableInit = () => {
new Sortable(sortableRef.value, {
animation: 150,
// swapThreshold: 1,
// fallbackOnBody: true,
onUpdate({ newIndex, oldIndex }) {
const newData = imgList.value[newIndex];
const oldData = imgList.value[oldIndex];
imgList.value[newIndex] = oldData;
imgList.value[oldIndex] = newData;
},
});
};
onMounted(() => {
if (props.sortable) {
sortableInit();
}
});
</script>
<style lang="less" scoped>
.max {
:deep(.el-upload) {
display: none;
}
}
.upload-box__sortable {
display: flex;
}
.sortable {
--img-size: 148px;
display: inline-flex;
flex-wrap: wrap;
margin: 0;
&--item {
overflow: hidden;
background-color: var(--el-fill-color-blank);
border: 1px solid #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: var(--img-size);
height: var(--img-size);
margin: 0 8px 8px 0;
padding: 0;
display: inline-flex;
position: relative;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
&-icon {
margin: 0 10px;
font-size: 20px;
}
&-hover {
background: rgba(0, 0, 0, 0.5);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
color: #fff;
align-items: center;
justify-content: center;
opacity: 0;
display: flex;
cursor: pointer;
}
&:hover {
.sortable--item-hover {
opacity: 1;
}
}
}
}
:deep(.el-upload) {
width: 148px;
height: 148px;
border: 1px dashed #c0ccda;
&:hover {
border-color: var(--el-color-primary-light-1);
color: var(--el-color-primary-light-1);
}
}
</style>

@ -2,36 +2,60 @@
<component :is="render" />
</template>
<script setup lang="jsx">
import SortableTable from './SortableTable.vue';
import { ElTable, ElTableColumn } from 'element-plus/es/components/table/index';
import 'element-plus/es/components/table/style/css';
import { ElTableColumn } from 'element-plus/es/components/table/index';
import ElTable from './extra/ElTable.vue';
const props = defineProps({
/**
* 列表唯一标识
*/
code: {
type: String,
default: '',
required: true,
},
/**
* 列表标题
*/
title: {
type: String,
default: '',
},
/**
* 是否支持拖拽排序
*/
sortable: {
type: Boolean,
default: false,
},
/**
* 列表配置
*/
config: {
type: Object,
required: true,
},
/**
* 列表操作
*/
operation: {
type: Array,
default() {
return ['create', 'remove', 'search'];
},
},
/**
* 列表总数据量
*/
total: {
type: Number,
default: 0,
},
/**
* 重置筛选条件支持异步函数
*/
reset: {
type: Function,
default: () => null,
},
});
const emits = defineEmits([
'create',
@ -42,7 +66,6 @@
'selectAll',
'selectionChange',
'currentChange',
'reset',
'template',
'import',
'export',
@ -189,7 +212,7 @@
const pages = computed(() => store.state.local.listPage);
const search = ref(
props.code
? store.state.local.listPage[props.code]
? _.cloneDeep(unref(pages)[props.code])
: {
pageIndex: 1,
length: 10,
@ -201,10 +224,13 @@
length: 10,
}
) => {
if (props.config.page?.sizes && !props.config.page?.sizes?.includes(page.length)) {
page.length = props.config.page.sizes[0];
}
if (props.code) {
store.commit('local/setListPage', {
...unref(pages),
[props.code]: page,
[props.code]: _.cloneDeep(page),
});
}
search.value = page;
@ -215,7 +241,7 @@
watch(
() => props.total,
(value) => {
if (!value) {
if (!value && unref(search).pageIndex !== 1) {
resetPage();
}
}
@ -224,10 +250,9 @@
const handleSearch = () => {
emits('search', unref(search));
};
const handleReset = () => {
emits('reset');
const handleReset = async () => {
await props.reset?.();
search.value = { pageIndex: 1, length: 10 };
handleSearch();
};
const handleTemplate = () => {
emits('template');
@ -267,13 +292,15 @@
//
watch(
search,
(value) => {
(value, old) => {
if (value && props.code) {
resetPage(value);
if (_.cloneDeep(value) !== _.cloneDeep(old)) {
resetPage(value);
}
}
handleSearch();
},
{ deep: true, immediate: props.config.autoSearch !== false }
{ deep: true, immediate: props.config.autoSearch === true }
);
//
const handleProxy = (fnName, args) => {
@ -310,6 +337,7 @@
defineExpose({
search: handleSearch,
selection,
checked,
refTable,
clearSelection,
toggleRowSelection,
@ -321,7 +349,6 @@
doLayout,
sort,
});
const Component = props.sortable ? SortableTable : ElTable;
const render = () => (
<div class="common-list">
{slots.search ? <div class="search-box">{slots.search()}</div> : ''}
@ -382,7 +409,7 @@
) : (
''
)}
{slots.operation?.()}
{slots.operation?.({ selection: unref(selection) })}
{props.operation.includes('import') ? (
<ElDialog
title={'模板导入 - ' + props.title || props.code}
@ -420,7 +447,7 @@
) : (
''
)}
{props.code ? (
{props.config.setting !== false ? (
<ElButton class="setting-btn" icon="setting" type="text" onClick={() => handleSetting()}>
<ElIcon name="Setting" />
<span>设置</span>
@ -516,8 +543,9 @@
</ElDialog>
</div>
<div class="content-box">
<Component
<ElTable
ref={refTable}
sortable={props.sortable}
border
stripe
highlightCurrentRow={false}
@ -549,7 +577,7 @@
''
)
)}
</Component>
</ElTable>
</div>
<div class="pagination-box">
{props.config.page === false ? (

@ -11,6 +11,11 @@
</script>
<style lang="less" scoped>
.el-button {
&.el-button--text {
background-color: transparent;
padding: 0;
height: unset;
}
:deep(.el-icon) {
position: relative;
top: -2px;

@ -1,17 +1,16 @@
<template>
<template v-if="name">
<svg v-if="svg" class="x-icon" aria-hidden="true">
<use :href="symbolId" :fill="color" />
</svg>
<i v-else-if="isRemix" class="x-icon" :class="'x-icon-' + name" v-bind="{ ...$props, ...$attrs }"></i>
<ElIcon v-else class="x-icon">
<ElIcon v-if="isElement" class="x-icon">
<component :is="icons[name]" />
</ElIcon>
<i v-else-if="isRemix" class="x-icon" :class="'x-icon-' + name" v-bind="{ ...$props, ...$attrs }"></i>
<component :is="svgs[name]" v-else class="x-icon" />
</template>
</template>
<script setup>
import * as icons from '@element-plus/icons';
import { icons, remix, svgs } from '@/icons';
import { ElIcon } from 'element-plus/es/components/icon/index';
const props = defineProps({
//
name: {
@ -34,12 +33,12 @@
default: 'inherit',
},
});
// SVG
const symbolId = computed(() => `#icon-${props.name}`);
//
const size = computed(() => (Number.isNaN(new Number(props.size).valueOf()) ? props.size : props.size + 'px'));
// remixelement-plus
const isRemix = computed(() => !Object.keys(icons).includes(props.name));
const color = computed(() => props.color);
//
const isElement = computed(() => !props.svg && Object.keys(icons).includes(props.name));
const isRemix = computed(() => !props.svg && remix.includes(props.name));
</script>
<style lang="less" scoped>
@ -51,5 +50,10 @@
svg.x-icon {
width: v-bind(size);
height: v-bind(size);
fill: v-bind(color);
:deep(image) {
width: 100%;
height: 100%;
}
}
</style>

@ -35,7 +35,7 @@
},
lazy: {
type: Boolean,
default: true,
default: false,
},
previewSrcList: {
type: Array,
@ -46,8 +46,10 @@
default: true,
},
});
const slots = useSlots();
const width = unref(computed(() => props.width));
const height = unref(computed(() => props.height));
const attrs = useAttrs();
const slots = useSlots();
const imageSlots = {
placeholder: () => (
<div class="image-slot">
@ -61,27 +63,61 @@
),
...slots,
};
if (props.previewSrcList?.length === 0) {
props.previewSrcList.push(props.src);
}
const width = computed(() => props.width);
const height = computed(() => props.height);
const render = () => <ElImage {...props} {...attrs} v-slots={imageSlots} />;
const previewSrcList = ref([]);
const count = computed(() => unref(previewSrcList).length);
watch(
() => props.previewSrcList,
(value) => {
if (value?.length === 0) {
previewSrcList.value.push(props.src);
} else {
previewSrcList.value = [...value];
}
},
{ immediate: true, deep: true }
);
const render = () =>
props.previewSrcList?.length > 0 ? (
<div class="image-container">
<ElImage {...{ ...props, previewSrcList: unref(previewSrcList) }} {...attrs} v-slots={imageSlots} />
<div class="image-count">{unref(count)}</div>
</div>
) : (
<ElImage {...{ ...props, previewSrcList: unref(previewSrcList) }} {...attrs} v-slots={imageSlots} />
);
</script>
<style lang="less" scoped>
.el-image {
.image-container {
position: relative;
width: v-bind(width);
height: v-bind(height);
:deep(.image-slot) {
display: flex;
justify-content: center;
border-radius: @layout-space;
overflow: hidden;
:deep(.image-count) {
border-radius: @layout-space-small;
background: #ddd;
padding: 0 7px;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f7fa;
color: var(--el-text-color-secondary);
font-size: 30px;
position: absolute;
bottom: 0;
right: 0;
}
}
.el-image {
width: v-bind(width);
height: v-bind(height);
}
:deep(.image-slot) {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: var(--el-text-color-secondary);
font-size: 30px;
}
</style>

@ -13,6 +13,10 @@
type: Boolean,
default: true,
},
maxlength: {
type: [Number, String],
default: 255,
},
placeholder: {
type: String,
default: '请输入',
@ -27,10 +31,6 @@
return props.type === 'password';
},
},
showWordLimit: {
type: Boolean,
default: true,
},
rows: {
type: Number,
default: 3,
@ -48,6 +48,7 @@
});
const attrs = useAttrs();
const slots = useSlots();
const render = () => <ElInput {...props} {...attrs} v-slots={slots}></ElInput>;
const showWordLimit = props.showWordLimit === true && props.maxlength !== 255;
const render = () => <ElInput {...{ ...props, showWordLimit }} {...attrs} v-slots={slots}></ElInput>;
</script>
<style lang="less" scoped></style>

@ -2,7 +2,7 @@
<component :is="render" />
</template>
<script setup lang="jsx">
import { ElSelect, ElOption } from 'element-plus/es/components/select/index';
import { ElOption, ElSelect } from 'element-plus/es/components/select/index';
import 'element-plus/es/components/select/style/css';
const props = defineProps({
opts: {

@ -0,0 +1,12 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import { ElSwitch } from 'element-plus/es/components/switch/index';
import 'element-plus/es/components/switch/style/css';
const props = defineProps({});
const attrs = useAttrs();
const slots = useSlots();
const render = () => <ElSwitch {...props} {...attrs} v-slots={slots} />;
</script>
<style lang="less" scoped></style>

@ -6,6 +6,13 @@
import 'element-plus/es/components/table/style/css';
import Sortable from 'sortablejs';
const props = defineProps({
/**
* 是否可拖拽排序
*/
sortable: {
type: Boolean,
default: false,
},
/**
* 拖拽表格唯一标识用于区分多个拖拽区域互相拖拽时数据来源
*/
@ -138,7 +145,7 @@
sortable.value = new Sortable(el, {
group: props.group,
sort: props.sort,
disabled: props.disabled,
disabled: !props.sortable || props.disabled,
animation: props.animation,
draggable: '.' + props.draggable,
handle: props.handle,
@ -219,8 +226,21 @@
console.error('可拖拽表格ID不存在');
}
};
// sortablejs
onMounted(handleInit);
if (props.sortable) {
// sortablejs
onMounted(handleInit);
} else {
watch(
() => props.sortable,
(value, old) => {
if (!old && value) {
handleInit();
} else if (old && !value) {
sortable.value.destroy();
}
}
);
}
//
const handleProxy = (fnName, args) => {
return unref(refsTable)[fnName]?.apply(unref(refsTable), args);

@ -0,0 +1,16 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import { ElTooltip } from 'element-plus/es/components/tooltip/index';
import 'element-plus/es/components/tooltip/style/css';
import ElIcon from './ElIcon.vue';
const props = defineProps({});
const attrs = useAttrs();
const slots = {
default: () => <ElIcon name="information" style="position: relative;top: 2px;margin-left: 5px;" />,
...useSlots(),
};
const render = () => <ElTooltip {...props} {...attrs} v-slots={slots} />;
</script>
<style lang="less" scoped></style>

@ -1,202 +0,0 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import config from '@/configs';
import { ElMessage } from '@/plugins/element-plus';
import { ElUpload } from 'element-plus/es/components/upload/index';
import 'element-plus/es/components/upload/style/css';
import { ElImage } from 'element-plus/es/components/image/index';
import 'element-plus/es/components/image/style/css';
const store = useStore();
const props = defineProps({
action: {
type: String,
default: config.baseURL + '/edu-oss/oss/fileUpload',
},
data: {
type: Object,
default() {
return { service: 'msb-edu-course' };
},
},
headers: {
type: Object,
default() {
return {};
},
},
drag: {
type: Boolean,
default: true,
},
multiple: {
type: Boolean,
},
limit: {
type: Number,
default: 1,
},
size: {
type: Number,
default: 1024 * 1024 * 20,
},
accept: {
type: String,
default: '*.*',
},
});
const attrs = useAttrs();
const emits = defineEmits(['update:modelValue']);
props.headers['Authorization'] = 'Bearer ' + store.state.local.token;
let imgList = ref([]);
const refsUpload = ref(null);
watch(
() => imgList,
() => {
if (props.limit === 1) {
emits('update:modelValue', unref(imgList)[0]?.response.data);
} else {
emits('update:modelValue', unref(imgList));
}
},
{ deep: true }
);
const handleSuccess = (res, file, list) => {
console.info('[upload] success', list);
imgList.value = list;
};
const handleRemove = (file, list) => {
console.info('[upload] remove', list);
imgList.value = list;
};
const handleExceed = (list) => {
console.info('[upload] exceed', list);
ElMessage.error('超出最大上传数量');
};
const handleBeforeUpload = (file) => {
console.info('[upload] upload', file);
if (file.size >= props.size) {
ElMessage.error('超出文件大小限制');
return false;
}
};
const fmtSize = computed(() => {
const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
let res = props.size,
unit = 0;
while (res >= 800) {
res /= 1024;
unit++;
}
return res + units[unit];
});
watch(
() => attrs.modelValue,
(value) => {
if (props.limit === 1 && value) {
imgList.value = [
{
name: value,
response: {
data: value,
},
},
];
} else {
imgList.value = value || [];
}
},
{ immediate: true, deep: true }
);
const handleDeleteImage = (index) => {
if (unref(refsUpload)) {
unref(refsUpload).handleRemove(imgList[index]);
} else {
unref(imgList).splice(index, 1);
}
};
const render = () => (
<div class="upload-box">
<div class="upload-image">
{unref(imgList).map((item, index) => (
<div class="img-li">
<ElImage src={item?.response?.data} alt={item.name} />
<div class="img-li-cover" onClick={() => handleDeleteImage(index)}>
<ElIcon class="upload-del-icon" name="delete-bin-fill" size="20" />
</div>
</div>
))}
{props.limit != unref(imgList).length ? (
<ElUpload
ref={refsUpload}
{...props}
{...attrs}
before-upload={handleBeforeUpload}
on-exceed={handleExceed}
on-remove={handleRemove}
on-success={handleSuccess}
show-file-list={false}
>
<ElIcon class="el-icon--upload" name="add-fill" size="20" />
</ElUpload>
) : (
''
)}
</div>
<div class="el-upload__tip">支持小于 {unref(fmtSize)} 文件</div>
</div>
);
</script>
<style lang="less" scoped>
.upload-box {
:deep(.upload-image) {
display: flex;
flex-wrap: wrap;
.img-li {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 150px;
height: 150px;
margin-right: 20px;
overflow: hidden;
.img-li-cover {
display: none;
}
&:hover {
.img-li-cover {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
.upload-del-icon {
color: #fff;
}
}
}
}
}
}
:deep(.el-upload) {
width: 150px;
height: 150px;
.el-upload-dragger {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.el-icon--upload {
margin: 0;
}
}
}
</style>

@ -3,6 +3,10 @@ export default {
* 接口请求地址前缀
*/
baseURL: import.meta.env.VITE_BASE_URL,
/**
* 接口请求地址前缀
*/
socketURL: import.meta.env.VITE_SOCKET_URL,
/**
* 接口请求超时时间
*/
@ -11,4 +15,8 @@ export default {
* 是否使用本地路由
*/
useLocalRouter: false,
/**
* 项目名称
*/
projectName: '马士兵严选管理平台',
};

@ -1,2 +1,10 @@
import 'virtual:svg-icons-register';
import '@/icons/remixicon.less';
import * as icons from '@element-plus/icons';
import remix from './index.json';
const svgs = Object.fromEntries(
Object.entries(import.meta.globEager('./svg/*.svg')).map((entry) => [
entry[0].split('/').pop().split('.').reverse().slice(1).reverse().join('.'),
entry[1].default,
])
);
export { svgs, icons, remix };

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 39 27">
<path d="M16.6898,0.404005C10.2421,-1.28552,3.85953,2.35948,2.44299,10.6741C1.76728,14.5851,0.000672783,26.5487,0.000672783,26.5487C-0.00265092,26.6066,0.00636506,26.6645,0.0271694,26.719C0.0479738,26.7735,0.0801294,26.8233,0.121666,26.8654C0.163202,26.9075,0.213246,26.941,0.268733,26.9639C0.324219,26.9867,0.383979,26.9985,0.444358,26.9984C0.444358,26.9984,3.5176,26.9984,3.5176,26.9984C4.60586,27.028,5.66212,26.6428,6.45595,25.927C7.24978,25.2112,7.71677,24.2228,7.75502,23.1774C7.75502,23.1774,10.1973,8.6209,10.1973,8.6209C10.1973,8.53094,11.6261,3.50929,15.4524,3.14558C18.4971,2.85226,20.4754,8.02643,20.7848,8.53094C20.8192,8.59148,20.8686,8.64299,20.9285,8.68096C20.9885,8.71893,21.0572,8.74222,21.1287,8.74878C21.2002,8.75534,21.2722,8.74497,21.3386,8.71859C21.4049,8.69221,21.4636,8.65061,21.5093,8.59743C21.5093,8.59743,22.9259,7.37722,22.9259,7.37722C23.451,6.7984,21.1674,1.57729,16.6898,0.404005C16.6898,0.404005,16.6898,0.404005,16.6898,0.404005Z" />
<path d="M38.9491,3.33903C38.7599,2.57391,38.3581,1.87459,37.788,1.31802C37.2179,0.761449,36.501599999999996,0.369203,35.7179,0.184443C33.718599999999995,-0.304518,31.2265,0.121351,28.447699999999998,2.15606C26.8321,3.33903,18.07555,11.2649,17.1385,11.2255C16.6619,11.2255,15.866217,10.6379,15.567331,10.6064C15.471319,10.5956,15.374132,10.6117,15.287141,10.6528C15.20015,10.6939,15.12695,10.7583,15.0761068,10.8386C15.0252633,10.9188,14.99887783,11.0116,15.0000365897,11.1059C15.00119535,11.2003,15.0298522,11.2924,15.082652,11.3714C15.405772,11.8525,15.833904,13.9936,16.34282,14.4905C17.10215,15.2318,18.61274,14.0962,18.62081,14.1198C18.62889,14.1435,29.3403,5.35402,30.4672,4.91632C32.4867,4.12767,32.0828,6.49361,31.6789,8.46522C31.6789,8.46522,28.851599999999998,26.5449,28.851599999999998,26.5449C28.848300000000002,26.6033,28.857300000000002,26.6618,28.8779,26.7167C28.8985,26.7716,28.9304,26.8218,28.9717,26.8643C29.012900000000002,26.9067,29.0625,26.9405,29.1176,26.9636C29.1726,26.9866,29.2319,26.9985,29.2919,26.9984C29.2919,26.9984,32.341300000000004,26.9984,32.341300000000004,26.9984C33.421099999999996,27.0282,34.4692,26.6399,35.2569,25.9182C36.0446,25.1964,36.5079,24.1999,36.5459,23.1459C36.5459,23.1459,39.4136,4.35638,38.9491,3.33903C38.9491,3.33903,38.9491,3.33903,38.9491,3.33903Z" />
<path d="M9.16075,1C5.8477,2.4841800000000003,3.25749,5.77196,2.41016,10.7387C1.74353,14.6342,0.000663741,26.5504,0.000663741,26.5504C-0.00261529,26.6081,0.00627952,26.6659,0.0268043,26.7201C0.0473291,26.7744,0.0790526,26.824,0.12003,26.8659C0.161008,26.9079,0.21038,26.9412,0.265121,26.964C0.319862,26.9868,0.378822,26.9985,0.43839,26.9984C0.43839,26.9984,3.47033,26.9984,3.47033,26.9984C4.54396,27.0279,5.58602,26.6442,6.36919,25.9313C7.15235,25.2183,7.61306,24.2338,7.6508,23.1925C7.6508,23.1925,10.0603,8.693570000000001,10.0603,8.693570000000001C10.2797,7.91803,10.5953,7.17114,11,6.46925C11,6.46925,9.16075,1,9.16075,1C9.16075,1,9.16075,1,9.16075,1Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -1,5 +1,6 @@
<template>
<el-scrollbar class="layout-aside">
<LayoutLogo />
<el-menu collapse :default-active="activeAside">
<el-menu-item v-for="(item, index) in asideList" :key="index" :index="item.name" @click="handleClick(item)">
<el-icon :name="item.meta.icon" size="30" />
@ -10,7 +11,8 @@
</template>
<script setup>
const router = useRouter();
import LayoutLogo from './logo.vue';
const { proxy } = getCurrentInstance();
const store = useStore();
const asideList = computed(() => store.getters['layout/asideList']);
@ -20,16 +22,21 @@
watch(
() => activeAside,
(value) => {
store.commit(
'layout/setMenuList',
unref(asideList).find((item) => item.name === unref(value))?.children || []
);
store.commit('layout/setMenuList', unref(asideList).find((item) => item.name === unref(value))?.children);
},
{ immediate: true, deep: true }
);
const handleClick = (item) => {
store.commit('layout/setActiveAside', item.name);
store.commit('layout/setAutoRouter', true);
if (unref(activeAside) === item.name) {
store.commit('layout/setActiveAside', null);
proxy.$nextTick(() => {
store.commit('layout/setActiveAside', item.name);
});
} else {
store.commit('layout/setActiveAside', item.name);
}
};
</script>
@ -38,14 +45,16 @@
flex: 1;
width: @layout-aside-width;
height: 100%;
background-color: @color-black;
padding: @layout-space 0;
box-sizing: border-box;
background-color: @layout-aside-bgc;
:deep(.el-scrollbar__view) {
height: 100%;
.el-menu {
--el-menu-bg-color: transparent;
--el-menu-text-color: @layout-aside-fc;
--el-menu-item-height: @layout-aside-item-size;
--el-menu-hover-bg-color: @layout-aside-fc2;
--el-menu-hover-bg-color: @layout-aside-bgc-hover;
border-right: none;
width: 100%;
display: flex;
@ -64,9 +73,11 @@
P {
line-height: 1;
}
&:hover {
color: @layout-aside-fc-hover;
}
&.is-active {
background-color: var(--el-menu-active-color);
color: @layout-aside-fc;
color: @layout-aside-fc-active;
}
+ .el-menu-item {
margin-top: @layout-space;

@ -2,13 +2,14 @@
<div class="layout-footer">© 2020 马士兵北京教育科技有限公司</div>
</template>
<script setup>
import Breakcrumb from './breakcrumb.vue';
</script>
<script setup></script>
<style lang="less" scoped>
.layout-footer {
width: 100%;
height: @layout-footer-height;
line-height: @layout-footer-height;
flex-shrink: 0;
padding: @layout-space-small 0;
display: flex;
align-items: center;

@ -25,12 +25,11 @@
<style lang="less" scoped>
.layout-header {
width: 100%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
background-color: @layout-header-bgc;
color: @layout-header-fc;
box-shadow: @layout-shadow;
.header-left,
.header-right {
height: @layout-header-height;
@ -38,6 +37,9 @@
display: flex;
align-items: center;
}
.header-right {
margin-left: auto;
}
.x-icon {
border-radius: @layout-border-radius;
cursor: pointer;

@ -1,12 +1,13 @@
<template>
<div class="layout-logo">
<el-icon name="msb" svg :size="size" />
<el-icon :color="color" name="logo" :size="size" svg />
</div>
</template>
<script setup>
import variables from '@/styles/globalVariables.module.less';
const size = variables.layoutLogoSize;
const size = computed(() => variables.layoutLogoSize);
const color = computed(() => variables.layoutLogoColor);
</script>
<style lang="less" scoped>
@ -16,9 +17,11 @@
display: flex;
align-items: center;
justify-content: center;
background-color: @color-black;
background-color: @layout-aside-bgc;
.x-icon {
border-radius: @layout-border-radius;
border-radius: @layout-border-radius-large;
padding: @layout-space-small;
background: @layout-logo-bg;
}
}
</style>

@ -1,5 +1,5 @@
<template>
<el-scrollbar class="layout-main" always>
<el-scrollbar ref="refsScrollbar" class="layout-main" max-height="100%">
<RouterView />
</el-scrollbar>
</template>
@ -10,6 +10,7 @@
<style lang="less" scoped>
.layout-main {
flex: 1;
margin: @layout-space-large;
background-color: @color-white;
border-radius: @layout-border-radius;
@ -17,12 +18,18 @@
> :deep(.el-scrollbar__wrap) {
> .el-scrollbar__view {
width: 100%;
height: 100%;
min-height: 100%;
> * {
// display: inline-block; // BFC
width: 100%;
height: 100%;
height: @layout-main-height;
padding: @layout-space-large;
&.form-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
}
}

@ -1,5 +1,5 @@
<template>
<el-menu-item v-if="!menuItem.children?.length" :index="props.menuItem.name" :route="{ name: props.menuItem.name }">
<el-menu-item v-if="!hasChildren" :index="props.menuItem.name" :route="{ name: props.menuItem.name }">
<el-icon :name="props.menuItem.meta.icon" size="20" />
<p>{{ props.menuItem.meta.title }}</p>
</el-menu-item>
@ -8,17 +8,20 @@
<el-icon :name="props.menuItem.meta.icon" size="20" />
<p>{{ props.menuItem.meta.title }}</p>
</template>
<MenuItem v-for="(item, index) in menuItem.children" :key="index" :menu-item="item" />
<menu-item v-for="(item, index) in menuItem.children" :key="index" :menu-item="item" />
</el-sub-menu>
</template>
<script setup>
<script setup lang="jsx">
const props = defineProps({
menuItem: {
type: Object,
required: true,
},
});
const hasChildren = computed(
() => (props.menuItem.children || []).filter((item) => item.meta.hidden !== true).length > 0
);
</script>
<style lang="less" scoped>
@ -31,20 +34,16 @@
min-width: unset;
border-radius: @layout-border-radius;
&:hover {
color: @layout-menu-hover-fc;
background-color: @layout-menu-active-bgc;
}
&.is-active {
background-color: @layout-menu-active-bgc;
color: @layout-menu-active-fc;
}
+ .el-menu-item {
margin-top: @layout-space-small;
}
}
.el-sub-menu {
border-radius: @layout-border-radius;
&.is-opened {
background-color: @color-ghost-black;
> :deep(.el-sub-menu__title) {
background-color: @layout-menu-hover-bgc;
}
@ -53,12 +52,16 @@
border-radius: @layout-border-radius;
margin-bottom: @layout-space-small;
}
+ .el-menu-item {
margin-top: @layout-space-small;
}
:deep(.el-menu) {
padding: 0 !important;
border-radius: @layout-border-radius;
color: @color-white3;
}
}
.el-menu-item + .el-menu-item,
.el-menu-item + .el-sub-menu,
.el-sub-menu + .el-menu-item,
.el-sub-menu + .el-sub-menu {
margin-top: @layout-space-small;
}
</style>

@ -1,22 +1,24 @@
<template>
<el-scrollbar class="layout-menu" :class="{ collapse: collapseMenu }">
<div class="title">马士兵严选后台管理平台</div>
<el-divider>{{ activeAsideName }}</el-divider>
<el-menu unique-opened :default-active="activeMenu" @select="handleSelect">
<MenuItem v-for="(item, index) in menuList" :key="index" :menu-item="item" />
</el-menu>
</el-scrollbar>
<div class="layout-menu" :class="{ collapse: collapseMenu }">
<layout-title />
<el-scrollbar>
<el-menu :default-active="activeMenu" unique-opened @select="handleSelect">
<menu-item v-for="(item, index) in menuList" :key="index" :menu-item="item" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup>
<script setup lang="jsx">
import MenuItem from './menu-item.vue';
import LayoutTitle from './title.vue';
const router = useRouter();
const store = useStore();
const activeAsideName = computed(() => store.getters['layout/activeAsideName']);
const activeAside = computed(() => store.state.layout.activeAside);
const menuList = computed(() => store.state.layout.menuList);
const menuList = computed(() => store.getters['layout/menuList']);
const activeMenu = computed(() => store.state.layout.activeMenu);
const collapseMenu = computed(() => store.getters['layout/collapseMenu']);
const autoRouter = computed(() => store.state.layout.autoRouter);
const handleSelect = (index) => {
// TODO
router.push({ name: index });
@ -25,10 +27,17 @@
watch(
() => unref(menuList),
(value) => {
if (value.length === 1) {
handleSelect(value[0].name);
} else if (value.length === 0) {
handleSelect(unref(activeAside));
if (unref(autoRouter)) {
if (value) {
if (value.length === 1) {
console.info('[router] menu push 1 ' + value[0].name);
handleSelect(value[0].name);
} else if (value.length === 0) {
console.info('[router] menu push 0 ' + unref(activeAside));
handleSelect(unref(activeAside));
}
store.commit('layout/setAutoRouter', false);
}
}
},
{
@ -40,13 +49,11 @@
<style lang="less" scoped>
.layout-menu {
flex: 1;
width: @layout-menu-width;
height: 100%;
background-color: @layout-menu-bgc;
box-shadow: @layout-shadow;
padding: 0 @layout-space;
transition: width 0.3s;
box-sizing: border-box;
&.collapse {
width: 0;
padding: 0;
@ -59,30 +66,15 @@
:deep(*) {
white-space: nowrap;
}
.title {
width: 100%;
height: @layout-header-height;
line-height: @layout-header-height;
font-size: @layout-h2;
color: @layout-menu-active-fc;
text-align: center;
.text-overflow();
}
.el-divider {
margin: 0 0 @layout-space-super 0;
border-color: @color-ghost-white;
:deep(.el-divider__text) {
color: @layout-menu-active-fc;
background-color: @layout-menu-bgc;
}
}
:deep(.el-scrollbar__view) {
height: 100%;
.el-menu {
--el-menu-bg-color: transparent;
--el-menu-text-color: @layout-menu-fc;
--el-menu-hover-bg-color: @layout-menu-hover-bgc;
border-right: none;
:deep(.el-scrollbar) {
height: calc(100% - @layout-header-height);
padding: @layout-space;
box-sizing: border-box;
.el-scrollbar__view {
height: 100%;
.el-menu {
border-right: none;
}
}
}
}

@ -1,8 +1,7 @@
<template>
<div class="layout-profile">
<el-avatar :src="userInfo?.avatar" />
<el-dropdown :opts="opts">
<span>{{ userInfo?.nickname || userInfo?.username }}</span>
<span>{{ userInfo?.employeeName }}</span>
</el-dropdown>
</div>
</template>
@ -11,12 +10,6 @@
const store = useStore();
const userInfo = computed(() => store.state.auth.userInfo);
const opts = reactive([
{
label: '个人中心',
onClick() {
alert('个人中心');
},
},
{
label: '退出登录',
onClick() {

@ -1,6 +1,6 @@
<template>
<div class="layout-tabs">
<el-tabs :modelValue="activeTab" type="card" class="demo-tabs" @tab-click="handleClick">
<el-tabs class="demo-tabs" :model-value="activeTab" type="card" @tab-click="handleClick">
<el-tab-pane v-for="(item, index) in tabList" :key="index" :name="item.name">
<template #label>
<el-icon class="tab-icon" :name="item.meta.icon" />
@ -15,7 +15,7 @@
</el-tab-pane>
</el-tabs>
<div class="operation">
<el-dropdown trigger="hover" :opts="opts" icon="apps-fill">
<el-dropdown icon="apps-fill" :opts="opts" trigger="hover">
<span></span>
</el-dropdown>
</div>
@ -28,15 +28,16 @@
const activeTab = computed(() => store.state.layout.activeTab);
const tabList = computed(() => store.state.layout.tabList);
//
watch(
() => unref(tabList),
(value) => {
if (!unref(activeTab) || value.findIndex((item) => item.name === unref(activeTab)) === -1) {
router.push({ name: value[0].name });
}
},
{ immediate: true, deep: true }
);
// watch(
// () => unref(tabList),
// (value) => {
// if (!unref(activeTab) || value.findIndex((item) => item.name === unref(activeTab)) === -1) {
// console.info('[router] tabs push ' + value[0].fullPath);
// router.push(value[0].fullPath);
// }
// },
// { immediate: true, deep: true }
// );
const handleCloseTab = (index) => {
store.commit('layout/closeTab', {
index,
@ -67,13 +68,14 @@
<style lang="less" scoped>
.layout-tabs {
width: 100%;
flex-shrink: 0;
height: @layout-tabs-height;
padding: 0 @layout-space-large;
display: flex;
align-items: flex-end;
justify-content: space-between;
overflow: hidden;
box-shadow: fade(@layout-header-fc, 15%) 0 1px 5px;
box-shadow: @layout-shadow;
background-color: @layout-header-bgc;
color: @layout-header-fc;
.el-tabs {
@ -145,7 +147,7 @@
}
}
.operation {
height: 80%;
height: 100%;
margin-left: @layout-space;
display: flex;
align-items: center;

@ -0,0 +1,42 @@
<template>
<div class="layout-title" :class="{ collapse: collapseMenu }">
<div class="title">{{ config.projectName }}</div>
</div>
</template>
<script setup>
import config from '@/configs';
const store = useStore();
const collapseMenu = computed(() => store.getters['layout/collapseMenu']);
</script>
<style lang="less" scoped>
.layout-title {
width: @layout-menu-width;
height: @layout-header-height;
background-color: @layout-menu-bgc;
padding: 0 @layout-space;
transition: width 0.3s;
color: @layout-title-fc;
&.collapse {
width: 0;
padding: 0;
overflow: hidden;
:deep(*) {
opacity: 0;
transition: opacity 0.3s;
}
}
:deep(*) {
white-space: nowrap;
}
.title {
width: 100%;
height: @layout-header-height;
line-height: @layout-header-height;
font-size: @layout-h2;
text-align: center;
.text-overflow();
}
}
</style>

@ -1,13 +1,26 @@
<template>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="fade-transform">
<keep-alive>
<component :is="Component" />
<router-view v-slot="{ route, Component }">
<transition mode="out-in" name="fade">
<keep-alive :exclude="exclude" :max="30">
<component :is="Component" :key="route.name" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup></script>
<script setup>
const store = useStore();
const exclude = computed(() => store.state.layout.notKeepAliveList);
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.1s ease-in;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

@ -1,35 +1,34 @@
<template>
<div class="layout-container layout-default">
<div class="layout-left">
<LayoutLogo />
<LayoutAside />
<layout-aside />
</div>
<div class="layout-center">
<LayoutMenu />
<layout-menu />
</div>
<div class="layout-right">
<LayoutHeader />
<LayoutTabs />
<LayoutMain />
<LayoutFooter />
<layout-header />
<layout-tabs />
<layout-main />
<layout-footer />
</div>
</div>
</template>
<script setup>
import LayoutMain from './components/main.vue';
import LayoutLogo from './components/logo.vue';
import LayoutAside from './components/aside.vue';
import LayoutMenu from './components/menu.vue';
import LayoutFooter from './components/footer.vue';
import LayoutHeader from './components/header.vue';
import LayoutMain from './components/main.vue';
import LayoutMenu from './components/menu.vue';
import LayoutTabs from './components/tabs.vue';
import LayoutFooter from './components/footer.vue';
</script>
<style lang="less" scoped>
.layout-container {
width: 100%;
height: 100%;
width: 100vw;
height: 100vh;
min-width: 1200px;
background-color: @color-white-dark;
}
.layout-default {
@ -38,6 +37,11 @@
display: flex;
flex-direction: column;
box-shadow: @layout-shadow;
z-index: 9;
}
.layout-center {
box-shadow: @layout-shadow;
z-index: 9;
}
.layout-right {
flex: 1;

@ -0,0 +1,124 @@
import { ElMessage } from './element-plus';
/**
* 复制文本
* @param {string} content 要复制的内容
*/
export const copy = async (content) => {
if (typeof navigator?.clipboard?.writeText === 'function') {
try {
await navigator.clipboard.writeText(content);
ElMessage.success('复制成功');
} catch (e) {
ElMessage.error('复制失败');
console.warn('复制失败', e);
}
} else {
const el = document.createElement('textarea');
el.style.position = 'absolute';
el.style.zIndex = '-9999';
el.value = content;
document.body.appendChild(el);
el.select();
if (document.execCommand('copy')) {
ElMessage.success('复制成功');
} else {
ElMessage.erorr('复制失败');
}
el.remove();
}
};
/**
* 下载指定地址的文件
* @param {*} fileName 文件名称
* @param {*} href 文件地址
*/
export const download = (fileName, href) => {
if ('download' in document.createElement('a')) {
const elink = document.createElement('a');
elink.download = fileName;
elink.style.display = 'none';
elink.href = href;
document.body.appendChild(elink);
elink.click();
document.body.removeChild(elink);
} else {
window.open(href, '_blank');
}
};
/**
* 导出为excel并下载
* @param {*} fileName 文件名称
* @param {*} data excel数据
*/
export const excel = (fileName, data) => {
const blob = new Blob([data], { type: 'text/plain,charset=UTF-8' });
if ('download' in document.createElement('a')) {
const elink = document.createElement('a');
elink.download = fileName;
elink.href = URL.createObjectURL(blob);
document.body.appendChild(elink);
elink.click();
URL.revokeObjectURL(elink.href);
document.body.removeChild(elink);
} else {
navigator.msSaveBlob(blob, fileName);
}
};
/**
* 字典查询
* @param {*} dict 字典数据
* @param {*} value 字典值
* @param {*} opts 配置
* @returns 字典标签
*/
export const dict = (dict, value, opts = {}) => {
value = value || [];
value = value instanceof Array ? value : [value];
opts = { label: 'label', value: 'value', default: '未知', ...opts };
return (
value
.map(
(v) =>
(dict ? dict.value || dict : []).find((item) => item[opts.value] === v)?.[opts.label] ||
opts.default
)
.join(',') || opts.default
);
};
/**
* 显示姓名
* @param {*} nickname 昵称
* @param {*} realname 真实姓名
* @returns 组合显示
*/
export const name = (nickname, realname) => {
return realname ? `${nickname}${realname}` : nickname;
};
/**
* 表单校验
* @param {*} refsForm 表单组件
*/
export const validate = (refsForm) => {
return new Promise((resolve, reject) => {
(async () => {
try {
await unref(refsForm).validate();
resolve();
} catch (e) {
console.info('校验失败', e);
unref(refsForm).scrollToField(Object.keys(e)[0]);
reject(e);
}
})();
});
};
export default (app) => {
app.config.globalProperties.$copy = copy;
app.config.globalProperties.$download = download;
app.config.globalProperties.$excel = excel;
app.config.globalProperties.$dict = dict;
app.config.globalProperties.$name = name;
app.config.globalProperties.$validate = validate;
};

@ -5,16 +5,27 @@ export default [
component: () => import('@/layouts/default.vue'),
meta: {
title: '组件示例',
icon: 'home-fill',
icon: 'test-tube-fill',
layout: true,
},
children: [
{
path: 'icon',
name: 'IconDemo',
component: () => import('@/views/demo/iconDemo.vue'),
meta: {
title: '图标库',
icon: 'aliens-fill',
keepAlive: false,
},
},
{
path: 'sortable',
name: 'SortableTableDemo',
component: () => import('@/views/demo/sortableTableDemo.vue'),
meta: {
title: '拖拽排序',
icon: 'home-fill',
icon: 'table-2',
},
},
],

@ -1,4 +1,8 @@
import config from '@/configs';
import store from '@/store';
import { createRouter, createWebHistory } from 'vue-router';
// 示例模块
import demoModule from './demo';
// 全局路由
export const globalRoutes = [
@ -20,41 +24,20 @@ export const globalRoutes = [
},
];
// 示例模块
import demoModule from './demo';
export const demeRoutes = import.meta.env.DEV ? demoModule : [];
export const demoRoutes = import.meta.env.MODE === 'development' ? demoModule : [];
// 动态模块
const dynamicRoutes = [];
export const dynamicRoutes = [];
const modules = import.meta.globEager('./modules/*.js');
Object.values(modules).forEach((mod) => {
dynamicRoutes.push(...mod.default);
dynamicRoutes.push(...(mod.default instanceof Array ? mod.default : [mod.default]));
});
// 本地路由
export const routes = [
...globalRoutes,
{
path: '/',
name: 'App',
redirect: { name: 'Home' },
component: () => import('@/layouts/default.vue'),
meta: {
layout: true,
},
children: [
{
path: '/home',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
icon: 'home-fill',
},
},
],
},
...dynamicRoutes,
...demoRoutes,
{
path: '/:pathMatch(.*)*',
redirect: '/404',
@ -65,30 +48,30 @@ export const routes = [
},
];
import config from '@/configs';
const router = createRouter({
history: createWebHistory(),
routes: config.useLocalRouter ? routes : [],
routes: config.useLocalRouter ? routes : globalRoutes,
});
import store from '@/store';
router.onError((error, to) => {
console.info('[router] error', error, to);
});
router.beforeEach(async (to, from, next) => {
if (!from.matched.length) {
store.loadCache();
}
if (store.state.local.token) {
if (!store.state.auth.permission.length) {
let res1 = true,
res2 = true;
res1 = await store.dispatch('auth/getUserInfo');
if (config.useLocalRouter) {
store.commit('auth/setPermission', routes);
} else {
await store.dispatch('auth/getUserInfo');
await store.dispatch('auth/getPermission');
res2 = await store.dispatch('auth/getPermission');
}
if (res1 && res2) {
next({ ...to, replace: true });
} else {
next(false);
}
next({ ...to, replace: true });
} else {
console.info(`[router] from ${from.name} to ${to.name}`);
const deep = (route) => {
@ -100,38 +83,102 @@ router.beforeEach(async (to, from, next) => {
next({ name: childName });
} else {
next();
store.commit('layout/setActiveAside', to.matched.find((item) => !item.meta?.layout).name);
// 面包屑导航算法
const deep = (tree, condition) => {
let path = [];
// 根据路由名称找路由
let target = tree.find(condition);
if (target) {
// 找到了加入路由路径
path.push(target);
// 继续从其他同级路由的子级中递归查找
let tempPath = deep(
tree.filter((item) => !condition(item)),
condition
);
if (tempPath.length) {
// 找到了回退路由路径
path.pop();
// 将新路由路径拼接进来
path.push(...tempPath);
}
} else {
// 没有找到,遍历查找这个层级所有元素
tree.find((item) => {
// 更新路由路径
path.push(item);
// 从子路由中递归查找
let tempPath = deep(item.children || [], condition);
if (tempPath.length) {
// 找到了拼接新路由路径
path.push(...tempPath);
} else {
// 没找到回退路由路径
path.pop();
}
});
}
return path;
};
const breakcrumbList = deep(store.state.auth.permission, (item) => item.name === to.name);
store.commit('layout/setActiveAside', to.matched[0].name);
store.commit('layout/setActiveMenu', to.name);
store.commit('layout/setActiveTab', to.name);
store.commit('layout/setBreakcrumbList', to.matched);
store.commit('layout/setBreakcrumbList', breakcrumbList);
store.commit('layout/addTab', to);
}
}
} else if (to.meta.global) {
next();
} else {
next({ name: 'Login' });
next(false);
store.dispatch('auth/logout', to.fullPath);
}
});
export default router;
export const reset = (routers) => {
/**
* 多级嵌套路由时由于父级路由页面中没有router-view组件所以子级路由页面无法显示
* 所以需要将路由规则展平处理
* @param {*} routes
* @param {*} parent
*/
const flatRoutes = (routes, parent = { path: '/', meta: {} }) =>
routes.flatMap((route) => {
let res = [];
if (!route.path.startsWith('/')) {
if (!parent.path.endsWith('/')) {
parent.path += '/';
}
route.path = parent.path + route.path;
}
route.meta.menu = route.path.replaceAll(/\/:[^?]+\?/g, '');
let activeMenu = route.meta.activeMenu;
if (!activeMenu && route.meta.hidden) {
activeMenu = parent.meta.menu;
}
route.meta.activeMenu = activeMenu || route.meta.menu;
const children = flatRoutes(route.children || [], route);
route.redirect = route.redirect || children.find((item) => !item.meta.hidden)?.meta.menu;
if (route.meta.layout || route.meta.view) {
route.children = children;
res = [route];
} else {
res = [route, ...children];
}
return res;
});
export const reset = (routes) => {
router.getRoutes().forEach((item) => {
router.removeRoute(item.name);
});
routers = [
...globalRoutes,
...routers,
{
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'NotFound',
meta: {
global: true,
},
},
];
routers.forEach(router.addRoute);
flatRoutes(routes).forEach(router.addRoute);
store.commit(
'layout/setNotKeepAliveList',
router
.getRoutes()
.filter((item) => item.meta.keepAlive === false)
.map((item) => item.name)
);
console.info('[router] reset', router.getRoutes());
};

@ -0,0 +1,32 @@
export default [
{
path: '/chat',
name: 'ChatManagement',
component: () => import('@/layouts/default.vue'),
meta: {
title: '客服会话',
icon: 'wechat-2-fill',
layout: true,
},
children: [
{
path: 'session',
name: 'ChatSession',
component: () => import('@/views/chat/index.vue'),
meta: {
title: '聊天会话',
icon: 'wechat-2-fill',
},
},
{
path: 'management',
name: 'CustomerServiceManagement',
component: () => import('@/views/chat/management.vue'),
meta: {
title: '客服管理',
icon: 'chat-smile-2-fill',
},
},
],
},
];

@ -1,286 +0,0 @@
export default [
{
path: '/education',
name: 'Education',
component: () => import('@/layouts/default.vue'),
meta: {
title: '教务教学',
icon: 'book-open-fill',
},
children: [
{
path: 'teacher',
name: 'TeacherManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '老师管理',
icon: 'user-shared-fill',
},
},
{
path: 'student',
name: 'StudentManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '学生管理',
icon: 'user-received-fill',
},
children: [
{
path: 'list',
name: 'StudentManagementList',
component: () => import('@/views/home/index.vue'),
meta: {
title: '学生列表',
icon: 'user-received-line',
},
},
{
path: 'grade',
name: 'GradeManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '年级管理',
icon: 'account-pin-box-fill',
},
},
{
path: 'class',
name: 'ClassManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '班级管理',
icon: 'account-pin-circle-fill',
},
},
],
},
{
path: 'admission',
name: 'AdmissionManagement',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '入学',
icon: 'award-fill',
},
children: [
{
path: 'evaluation',
name: 'AdmissionEvaluation',
component: () => import('@/views/home/index.vue'),
meta: {
title: '入学评测',
icon: 'file-paper-2-fill',
},
},
{
path: 'way',
name: 'StudyWay',
component: () => import('@/views/home/index.vue'),
meta: {
title: '学习路线',
icon: 'send-plane-fill',
},
},
],
},
{
path: 'answer',
name: 'Answer',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑',
icon: 'question-answer-fill',
},
children: [
{
path: 'management',
name: 'AnswerManagement',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑管理',
icon: 'question-answer-fill',
},
children: [
{
path: 'list',
name: 'AnswerManagementList',
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑管理列表',
icon: 'question-answer-line',
},
},
{
path: 'invalid',
name: 'InvalidQuestionManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '无效追问管理',
icon: 'questionnaire-fill',
},
},
{
path: 'lock',
name: 'UserLockManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '用户锁定管理',
icon: 'lock-fill',
},
},
],
},
{
path: 'assign/:questionId',
name: 'AssignAnswerTeacher',
component: () => import('@/views/home/index.vue'),
meta: {
title: '指派老师',
icon: 'account-box-fill',
hidden: true,
activeMenu: '/education/answer/management',
},
},
{
path: 'teacher',
name: 'AnswerTeacher',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑老师管理',
icon: 'book-line',
},
children: [
{
path: 'summary',
name: 'AnswerTeacherSummary',
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑老师统计',
icon: 'book-2-line',
},
},
{
path: 'management',
name: 'AnswerTeacherManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑老师管理',
icon: 'book-3-line',
},
},
{
path: 'bind/:teacher?',
name: 'AnswerTeacherUpdateBind',
component: () => import('@/views/home/index.vue'),
meta: {
title: '编辑课程绑定',
icon: 'book-fill',
hidden: true,
activeMenu: '/education/answer/teacher/management',
},
},
{
path: 'course',
name: 'AnswerTeacherBindCourse',
component: () => import('@/views/home/index.vue'),
meta: {
title: '绑定课程管理',
icon: 'book-fill',
},
},
],
},
],
},
{
path: 'question',
name: 'QuestionManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '题库管理',
icon: 'brush-fill',
},
},
{
path: 'note',
name: 'NoteManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '笔记管理',
icon: 'sticky-note-2-fill',
},
},
{
path: 'research',
name: 'CourseResearch',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '课研更新',
icon: 'contacts-book-upload-fill',
},
children: [
{
path: 'plan',
name: 'CourseResearchPlan',
component: () => import('@/views/home/index.vue'),
meta: {
title: '课研计划',
icon: 'contacts-book-upload-line',
},
},
{
path: 'log',
name: 'CourseResearchLog',
component: () => import('@/views/home/index.vue'),
meta: {
title: '课研更新日志',
icon: 'file-copy-2-line',
},
},
],
},
{
path: 'live',
name: 'LiveManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '直播管理',
icon: 'live-fill',
},
},
{
path: 'material',
name: 'MaterialManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '老师资料管理',
icon: 'database-2-fill',
},
},
{
path: 'shift',
name: 'ShiftTable',
component: () => import('@/views/home/index.vue'),
meta: {
title: '排班表',
icon: 'calendar-check-line',
},
},
{
path: 'course',
name: 'CourseTable',
component: () => import('@/views/home/index.vue'),
meta: {
title: '课程表管理',
icon: 'calendar-todo-fill',
},
},
],
},
];

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

Loading…
Cancel
Save