This commit is contained in:
zhangtao 2022-10-19 11:42:47 +08:00
commit e0b93a88c2
97 changed files with 26536 additions and 0 deletions

8
.env Normal file
View File

@ -0,0 +1,8 @@
# title
VITE_APP_TITLE = '智飞'
# 端口号
VITE_PORT = 3050
VITE_SERVER = "/pilot/admin"

14
.env.localhost Normal file
View File

@ -0,0 +1,14 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# 是否启用MOCK
VITE_APP_USE_MOCK = false
# proxy
VITE_PROXY = [["/api-local","http://127.0.0.1:8002"],["/api-mock","http://127.0.0.1:8003"]]
# base api
VITE_APP_GLOB_BASE_API = '/api-local'
# mock base api
VITE_APP_GLOB_BASE_API_MOCK = '/api-mock'

329
.eslintrc.js Normal file
View File

@ -0,0 +1,329 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true
},
extends: [
'plugin:vue/vue3-recommended',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
/* 强制每行的最大属性 */
'vue/max-attributes-per-line': [2, {
/* 当开始标签位于单行时,每行的最大属性数 */
'singleline': 10,
/* 当开始标签位于多行时,每行的最大属性数 */
'multiline': {
'max': 1
}
}],
/* 在单行元素的内容之前和之后需要换行符 */
'vue/singleline-html-element-content-newline': 'off',
/* 在单行元素的内容之前和之后需要换行符 */
'vue/multiline-html-element-content-newline': 'off',
/* 组件名称驼峰 */
'vue/component-definition-name-casing': ['error', 'PascalCase'],
/* 禁止使用 v-html */
'vue/no-v-html': 'off',
/* 格式化箭头函数的箭头前后空格 */
'arrow-spacing': [2, {
'before': true,
'after': true
}],
/* 强制缩进 */
'block-spacing': [2, 'always'],
/* 强制执行一个真正的大括号风格 */
'brace-style': [2, '1tbs', {
/* 允许一个块打开和关闭括号在同一行上 */
'allowSingleLine': true
}],
/* 为属性名称强制执行 camelcase 样式 */
'camelcase': [0, {
'properties': 'always'
}],
/* 不允许对象和数组尾随逗号 */
'comma-dangle': [2, 'never'],
/* 对象逗号前后允许空格 */
'comma-spacing': [2, {
'before': false,
'after': true
}],
/* 在数组元素、对象属性或变量声明之后和同一行上需要逗号 */
'comma-style': [2, 'last'],
/* 检查是否存在有效的super()调用 */
'constructor-super': 2,
/* 以允许支柱少单行ifelse ifelseforwhile或do同时还规定使用其他实例花括号 */
'curly': [2, 'multi-line'],
/* 成员表达式中的点应与属性部分位于同一行 */
'dot-location': [2, 'property'],
/* 在非空文件的末尾至少执行一个换行符 */
'eol-last': 2,
/* 强制使用===和!== */
'eqeqeq': ['error', 'always'],
/* 强化*发生器功能的间距 */
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
/* 使用回调模式时会处理这个错误 */
'handle-callback-err': [2, '^(err|error)$'],
/* 强制执行一致的缩进样式 */
'indent': [2, 2, {
/* 强制缩进级别case的条款switch声明 */
'SwitchCase': 1
}],
/* 一致使用单引号 */
'jsx-quotes': [2, 'prefer-single'],
/* 强制在对象字面量属性中的键和值之间保持一致的间距 */
'key-spacing': [2, {
/* 不允许在对象文字中的键和冒号之间使用空格 */
'beforeColon': false,
/* 需要在冒号和对象文字中的值之间至少有一个空格 */
'afterColon': true
}],
/* 强制执行围绕关键字和关键字标记的一致空格 */
'keyword-spacing': [2, {
'before': true,
'after': true
}],
/* 要求构造函数名以大写字母开头 */
'new-cap': [2, {
/* 要求new使用大写启动函数调用所有操作符 */
'newIsCap': true,
/* 允许在没有new操作符的情况下调用大写启动的函数 */
'capIsNew': false
}],
/* 在使用new关键字调用不带参数的构造函数时需要括号以便提高代码清晰度 */
'new-parens': 2,
/* 不允许使用Array构造函数 */
'no-array-constructor': 2,
/* 阻止使用已弃用和次优代码 */
'no-caller': 2,
/* 禁止调用console对象的方法 */
'no-console': 'off',
/* 标记修改类声明的变量 */
'no-class-assign': 2,
/* 不允许在试验条件不明确赋值运算符ifforwhile和do...while语句 */
'no-cond-assign': 2,
/* 标记修改使用const关键字声明的变量 */
'no-const-assign': 2,
/* 不允许正则表达式中的控制字符 */
'no-control-regex': 0,
/* 不允许在变量上使用delete操作符 */
'no-delete-var': 2,
/* 不允许在函数声明或表达式中使用重复的参数名称 */
'no-dupe-args': 2,
/* 标记在级别成员中使用重复名称 */
'no-dupe-class-members': 2,
/* 不允许在对象文字中使用重复键 */
'no-dupe-keys': 2,
/* 不允许在switch语句的case子句中使用重复的测试表达式 */
'no-duplicate-case': 2,
/* 不允许在正则表达式中使用空字符类 */
'no-empty-character-class': 2,
/* 不允许空的解构模式 */
'no-empty-pattern': 2,
/* 禁止使用eval()函数来防止潜在的危险 */
'no-eval': 2,
/* 禁止对 catch 子句中的异常重新赋值 */
'no-ex-assign': 2,
/* 不允许直接修改内建对象的原型 */
'no-extend-native': 2,
/* 避免不必要的使用 */
'no-extra-bind': 2,
/* 禁止不必要的布尔转换 */
'no-extra-boolean-cast': 2,
/* 仅在函数表达式附近禁止不必要的括号 */
'no-extra-parens': [2, 'functions'],
/* 消除一个案件无意中掉到另一个案件 */
'no-fallthrough': 2,
/* 消除浮点小数点,并在数值有小数点但在其之前或之后缺少数字时发出警告 */
'no-floating-decimal': 2,
/* 不允许重新分配function声明 */
'no-func-assign': 2,
/* 消除隐含eval()通过使用setTimeout()setInterval()或execScript() */
'no-implied-eval': 2,
/* 不允许function嵌套块中的声明 */
'no-inner-declarations': [2, 'functions'],
/* 不允许RegExp构造函数中的无效正则表达式字符串 */
'no-invalid-regexp': 2,
/* 捕获不是正常制表符和空格的无效空格 */
'no-irregular-whitespace': 2,
/* 防止因使用 __iterator__属性而出现的错误 */
'no-iterator': 2,
/* 禁用与变量同名的标签 */
'no-label-var': 2,
/* 禁用标签语句 */
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
/* 禁用不必要的嵌套块 */
'no-lone-blocks': 2,
/* 禁止混用tab和space */
'no-mixed-spaces-and-tabs': 2,
/* 禁止出现多个空格 */
'no-multi-spaces': 2,
/* 禁止多行字符串 */
'no-multi-str': 2,
/* 强制最大连续空行数1 */
'no-multiple-empty-lines': [2, {
'max': 1
}],
/* 不允许修改只读全局变量 */
'no-global-assign': 2,
/* 不允许使用Object构造函数 */
'no-new-object': 2,
/* 消除new require表达的使用 */
'no-new-require': 2,
/* 防止Symbol与new操作员的意外呼叫 */
'no-new-symbol': 2,
/* 杜绝使用StringNumber以及Boolean与new操作 */
'no-new-wrappers': 2,
/* 不允许调用MathJSON和Reflect对象作为功能 */
'no-obj-calls': 2,
/* 不允许使用八进制文字 */
'no-octal': 2,
/* 不允许字符串文字中的八进制转义序列 */
'no-octal-escape': 2,
/* 防止 Node.js 中的目录路径字符串连接 */
'no-path-concat': 2,
/* 当一个对象被__proto__创建时被设置为该对象的构造函数的原始原型属性。getPrototypeOf是获得“原型”的首选方法 */
'no-proto': 2,
/* 消除在同一范围内具有多个声明的变量 */
'no-redeclare': 2,
/* 在正则表达式文字中不允许有多个空格 */
'no-regex-spaces': 2,
/* return陈述中的任务除非用圆括号括起来否则不允许赋值 */
'no-return-assign': [2, 'except-parens'],
/* 消除自我分配 */
'no-self-assign': 2,
/* 禁止自身比较 */
'no-self-compare': 2,
/* 不允许使用逗号操作符 */
'no-sequences': 2,
/* 关键字不能被遮蔽 */
'no-shadow-restricted-names': 2,
/* 不允许功能标识符与其应用程序之间的间距 */
'no-spaced-func': 2,
/* 禁用稀疏数组 */
'no-sparse-arrays': 2,
/* 在构造函数中禁止在调用super()之前使用this或super。 */
'no-this-before-super': 2,
/* 限制可以被抛出的异常 */
'no-throw-literal': 2,
/* 禁用行尾空白 */
'no-trailing-spaces': 2,
/* 禁用未声明的变量 */
'no-undef': 2,
/* 禁用未声明的变量 */
'no-undef-init': 2,
/* 禁止使用令人困惑的多行表达式 */
'no-unexpected-multiline': 2,
/* 禁用一成不变的循环条件 */
'no-unmodified-loop-condition': 2,
/* 禁止可以表达为更简单结构的三元操作符 */
'no-unneeded-ternary': [2, {
/* 禁止条件表达式作为默认的赋值模式 */
'defaultAssignment': false
}],
/* 禁止在 return、throw、continue 和 break 语句后出现不可达代码 */
'no-unreachable': 2,
/* 禁止在 finally 语句块中出现控制流语句 */
'no-unsafe-finally': 2,
/* 禁止未使用过的变量 */
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
/* 禁用不必要的 .call() 和 .apply() */
'no-useless-call': 2,
/* 禁止在对象中使用不必要的计算属性 */
'no-useless-computed-key': 2,
/* 禁用不必要的构造函数 */
'no-useless-constructor': 2,
/* 在不改变代码行为的情况下可以安全移除的转义 */
'no-useless-escape': 0,
/* 禁止属性前有空白 */
'no-whitespace-before-property': 2,
/* 禁用 with 语句 */
'no-with': 2,
/* 强制函数中的变量在一起声明或分开声明 */
'one-var': [2, {
/* 要求每个作用域的初始化的变量有多个变量声明 */
'initialized': 'never'
}],
/* 强制操作符使用一致的换行符风格 */
'operator-linebreak': [2, 'after', {
/* 覆盖对指定的操作的全局设置 */
'overrides': {
'?': 'before',
':': 'before'
}
}],
/* 要求或禁止块内填充 */
'padded-blocks': [2, 'never'],
/* 强制使用一致的反勾号、双引号或单引号 */
'quotes': [2, 'single', {
/* 允许字符串使用单引号或双引号,只要字符串中包含了一个其它引号,否则需要转义 */
'avoidEscape': true,
/* 允许字符串使用反勾号 */
'allowTemplateLiterals': true
}],
/* 要求或禁止使用分号代替 ASI */
'semi': [2, 'never'],
/* 强制分号前后有空格 */
'semi-spacing': [2, {
'before': false,
'after': true
}],
/* 要求或禁止语句块之前的空格 */
'space-before-blocks': [2, 'always'],
/* 要求或禁止函数圆括号之前有一个空格 */
'space-before-function-paren': [2, 'never'],
/* 禁止或强制圆括号内的空格 */
'space-in-parens': [2, 'never'],
/* 要求中缀操作符周围有空格 */
'space-infix-ops': 2,
/* 要求或禁止在一元操作符之前或之后存在空格 */
'space-unary-ops': [2, {
/* 适用于单词类一元操作符例如new、delete、typeof、void、yield */
'words': true,
/* 适用于这些一元操作符: -、+、--、++、!、!! */
'nonwords': false
}],
/* 要求或禁止在注释前有空白 */
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
/* 强制模板字符串中空格的使用 禁止花括号内出现空格 */
'template-curly-spacing': [2, 'never'],
/* 要求调用 isNaN()检查 NaN */
'use-isnan': 2,
/* 强制 typeof 表达式与有效的字符串进行比较 */
'valid-typeof': 2,
/* 需要把立即执行的函数包裹起来 */
'wrap-iife': [2, 'any'],
/* 强制在 yield* 表达式中 * 周围使用空格 */
'yield-star-spacing': [2, 'both'],
/* 要求或者禁止Yoda条件 */
'yoda': [2, 'never'],
/* 建议使用const */
'prefer-const': 2,
/* 禁用 debugger */
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
/* 强制在花括号中使用一致的空格 */
'object-curly-spacing': [2, 'always', {
/* 禁止以对象元素开始或结尾的对象的花括号中有空格 */
objectsInObjects: false
}],
/* 禁止或强制在括号内使用空格 */
'array-bracket-spacing': [2, 'never']
}
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)

3
build/constant.js Normal file
View File

@ -0,0 +1,3 @@
export const GLOB_CONFIG_FILE_NAME = 'app.config.js'
export const GLOB_CONFIG_NAME = '__APP__GLOB__CONF__'
export const OUTPUT_DIR = 'dist'

View File

@ -0,0 +1,14 @@
import chalk from 'chalk'
import { writeFileSync } from 'fs-extra'
import { OUTPUT_DIR } from '../constant'
import { getEnvConfig, getRootPath } from '../utils'
export function runBuildCNAME() {
const { VITE_APP_GLOB_CNAME } = getEnvConfig()
if (!VITE_APP_GLOB_CNAME) return
try {
writeFileSync(getRootPath(`${OUTPUT_DIR}/CNAME`), VITE_APP_GLOB_CNAME)
} catch (error) {
console.log(chalk.red('CNAME file failed to package:\n' + error))
}
}

View File

@ -0,0 +1,29 @@
import { GLOB_CONFIG_FILE_NAME, GLOB_CONFIG_NAME, OUTPUT_DIR } from '../constant'
import fs, { writeFileSync } from 'fs-extra'
import chalk from 'chalk'
import { getEnvConfig, getRootPath } from '../utils'
function createConfig(option) {
const { config, configName, configFileName } = option
try {
const windowConf = `window.${configName}`
const configStr = `${windowConf}=${JSON.stringify(config)};
Object.freeze(${windowConf});
Object.defineProperty(window, "${configName}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, '')
fs.mkdirp(getRootPath(OUTPUT_DIR))
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr)
} catch (error) {
console.log(chalk.red('configuration file configuration file failed to package:\n' + error))
}
}
export function runBuildConfig() {
const config = getEnvConfig()
const configName = GLOB_CONFIG_NAME
const configFileName = GLOB_CONFIG_FILE_NAME
createConfig({ config, configName, configFileName })
}

16
build/script/index.js Normal file
View File

@ -0,0 +1,16 @@
import chalk from 'chalk'
import { runBuildConfig } from './build-config'
import { runBuildCNAME } from './build-cname'
export const runBuild = async () => {
try {
runBuildConfig()
runBuildCNAME()
console.log(`${chalk.cyan('build successfully!')}`)
} catch (error) {
console.log(chalk.red('vite build error:\n' + error))
process.exit(1)
}
}
runBuild()

71
build/utils.js Normal file
View File

@ -0,0 +1,71 @@
import fs from 'fs'
import path from 'path'
import dotenv from 'dotenv'
export function wrapperEnv(envOptions) {
if (!envOptions) return {}
const ret = {}
for (const key in envOptions) {
let val = envOptions[key]
if (['true', 'false'].includes(val)) {
val = val === 'true'
}
if (['VITE_PORT'].includes(key)) {
val = +val
}
if (key === 'VITE_PROXY' && val) {
try {
val = JSON.parse(val.replace(/'/g, '"'))
} catch (error) {
val = ''
}
}
ret[key] = val
if (typeof key === 'string') {
process.env[key] = val
} else if (typeof key === 'object') {
process.env[key] = JSON.stringify(val)
}
}
return ret
}
/**
* 获取当前环境下生效的配置文件名
*/
function getConfFiles() {
const script = process.env.npm_lifecycle_script
const reg = new RegExp('--mode ([a-z_\\d]+)')
const result = reg.exec(script)
if (result) {
const mode = result[1]
return ['.env', '.env.local', `.env.${mode}`]
}
return ['.env', '.env.local', '.env.production']
}
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) {
let envConfig = {}
confFiles.forEach((item) => {
try {
if (fs.existsSync(path.resolve(process.cwd(), item))) {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)))
envConfig = { ...envConfig, ...env }
}
} catch (e) {
console.error(`Error in parsing ${item}`, e)
}
})
const reg = new RegExp(`^(${match})`)
Object.keys(envConfig).forEach((key) => {
if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key)
}
})
return envConfig
}
export function getRootPath(...dir) {
return path.resolve(process.cwd(), ...dir)
}

31
build/vite/plugin/html.js Normal file
View File

@ -0,0 +1,31 @@
import html from 'vite-plugin-html'
import { version } from '../../../package.json'
import { GLOB_CONFIG_FILE_NAME } from '../../constant'
export function configHtmlPlugin(viteEnv, isBuild) {
const { VITE_APP_TITLE, VITE_PUBLIC_PATH } = viteEnv
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`
const getAppConfigSrc = () => {
return `${path}${GLOB_CONFIG_FILE_NAME}?v=${version}-${new Date().getTime()}`
}
const htmlPlugin = html({
minify: isBuild,
inject: {
data: {
title: VITE_APP_TITLE
},
tags: isBuild
? [
{
tag: 'script',
attrs: {
src: getAppConfigSrc()
}
}
]
: []
}
})
return htmlPlugin
}

View File

@ -0,0 +1,26 @@
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
import { unocss } from './unocss'
import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [
vue(),
Components({
resolvers: [NaiveUiResolver()]
}),
VueSetupExtend(),
unocss(),
configHtmlPlugin(viteEnv, isBuild)
]
viteEnv?.VITE_APP_USE_MOCK && plugins.push(configMockPlugin(isBuild))
return plugins
}

14
build/vite/plugin/mock.js Normal file
View File

@ -0,0 +1,14 @@
import { viteMockServe } from 'vite-plugin-mock'
export function configMockPlugin(isBuild) {
return viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: !isBuild,
prodEnabled: isBuild,
injectCode: `
import { setupProdMockServer } from '../mock/_create-prod-server';
setupProdMockServer();
`
})
}

View File

@ -0,0 +1,9 @@
import Unocss from 'unocss/vite'
import { presetUno, presetAttributify, presetIcons } from 'unocss'
// https://github.com/antfu/unocss
export function unocss() {
return Unocss({
presets: [presetUno(), presetAttributify(), presetIcons()],
})
}

17
build/vite/proxy.js Normal file
View File

@ -0,0 +1,17 @@
const httpsRE = /^https:\/\//
export function createProxy(list = []) {
const ret = {}
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target)
// https://github.com/http-party/node-http-proxy#options
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
// https is require secure=false
...(isHttps ? { secure: false } : {})
}
}
return ret
}

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.9.21/skins/default/aliplayer-min.css" />
<script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/de/prismplayer/2.9.21/aliplayer-h5-min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,14 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.globEager('./**/*.js')
const mockModules = []
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return
}
mockModules.push(...modules[key].default)
})
export function setupProdMockServer() {
createProdMockServer(mockModules)
}

10
mock/_util.js Normal file
View File

@ -0,0 +1,10 @@
import Mock from 'mockjs'
export function resultSuccess(data, { message = 'ok' } = {}) {
return Mock.mock({
code: 0,
data,
message,
type: 'success'
})
}

21189
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "vite_vue3",
"version": "0.0.0",
"scripts": {
"dev": "vite --mode localhost",
"build:test": "vite build --mode test && esno ./build/script",
"build:dev": "vite build --mode development && esno ./build/script",
"build:prod": "vite build --mode production && esno ./build/script",
"serve": "vite preview"
},
"dependencies": {
"@tinymce/tinymce-vue": "^4.0.5",
"@vicons/antd": "^0.10.0",
"@vicons/ionicons5": "^0.10.0",
"ali-oss": "^6.17.1",
"axios": "^0.26.1",
"dayjs": "^1.11.2",
"mockjs": "^1.1.0",
"pinia": "^2.0.13",
"pinia-plugin-persist": "^1.0.0",
"tinymce": "^5.10.2",
"vue": "^3.2.16",
"vue-router": "^4.0.14",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@unocss/preset-attributify": "^0.16.4",
"@unocss/preset-icons": "^0.16.4",
"@unocss/preset-uno": "^0.16.4",
"@vitejs/plugin-vue": "^1.9.3",
"@vue/cli-plugin-eslint": "^5.0.4",
"babel-eslint": "^10.1.0",
"chalk": "^5.0.1",
"dotenv": "^10.0.0",
"eslint": "^7.19.0",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-vue": "^8.5.0",
"esno": "^0.13.0",
"fs-extra": "^10.0.1",
"naive-ui": "^2.27.0",
"sass": "^1.49.11",
"unocss": "^0.16.4",
"unplugin-vue-components": "^0.18.5",
"vite": "^2.6.4",
"vite-plugin-html": "^2.1.2",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-vue-setup-extend": "^0.4.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

42
src/App.vue Normal file
View File

@ -0,0 +1,42 @@
<!--
* @Author: whyafterme
* @Date: 2022-10-19 11:22:44
* @LastEditTime: 2022-10-19 11:31:00
* @LastEditors: whyafterme
* @Description:
* @FilePath: \gis\src\App.vue
-->
<template>
<n-config-provider :locale="zhCN" :date-locale="dateZhCN" inline-theme-disabled :theme-overrides="themeOverrides">
<n-loading-bar-provider>
<loading-bar />
<n-dialog-provider>
<dialog-content />
<n-message-provider>
<message-content />
</n-message-provider>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>
<script setup>
import { zhCN, dateZhCN } from 'naive-ui'
import themeOverrides from '@/utils/ui/theme.js'
import loadingBar from '@/components/LoadingBar/index.vue'
import messageContent from '@/components/Message/index.vue'
import dialogContent from '@/components/Dialog/index.vue'
</script>
<style lang="scss">
#app {
height: 100%;
.n-config-provider {
height: inherit;
}
}
</style>

23
src/api/auth/index.js Normal file
View File

@ -0,0 +1,23 @@
import { defAxios as request } from '@/utils/http'
export const login = (data) => {
return request({
url: '/auth/login',
method: 'post',
data
})
}
export const refreshToken = () => {
return request({
url: '/auth/refreshToken',
method: 'post'
})
}
export function getMenu() {
return request({
url: '/index/getMenuList',
method: 'GET'
})
}

45
src/api/common/upload.js Normal file
View File

@ -0,0 +1,45 @@
import { defAxios as request } from '@/utils/http'
/**
* @description: 获取音视频上传地址和凭证
* @param {Object} params
* @return {Object}
*/
export function getAuth(params) {
return request({
url: '/aliyuncsVod/createUploadVideo',
method: 'get',
params
})
}
/**
* @description: 刷新音/视频上传凭证
* @param {String} videoId
* @return {*}
*/
export function refreshAuth(videoId) {
return request({
url: '/aliyuncsVod/refreshUploadVideo',
method: 'get',
params: {
videoId
}
})
}
/**
* @description: 获取图片上传鉴权
* @param {String} objectName
* @return {*}
*/
export function getOssAuth(objectName) {
return request({
url: '/aliyunOss/getSecurityToken',
method: 'get',
responseAll: true,
params: {
objectName
}
})
}

9
src/api/home/index.js Normal file
View File

@ -0,0 +1,9 @@
import { defAxios as request } from '@/utils/http'
export function updatePwd(data) {
return request({
url: '/index/updatePwd',
method: 'PUT',
data
})
}

47
src/api/login/index.js Normal file
View File

@ -0,0 +1,47 @@
import { defAxios as request } from '@/utils/http'
/**
* 登录接口
* @param {Object} 用户名以及密码
* @returns 返回token信息
*/
export function userLogin(data = {}) {
return request({
url: '/login/login',
method: 'post',
data
})
}
/**
* params
* @returns 当前登录人信息
*/
export function getUser() {
return request({
url: '/index/getUserInfo',
method: 'get'
})
}
/**
* 获取验证码
* @returns 验证码图片
*/
export function userCaptcha() {
return request({
url: '/login/captcha',
method: 'get'
})
}
/**
* 退出登录
* @returns
*/
export function loginOut() {
return request({
url: '/login/logout',
method: 'GET'
})
}

32
src/api/user/index.js Normal file
View File

@ -0,0 +1,32 @@
import { defAxios as request } from '@/utils/http'
export function getUsers(data = {}) {
return request({
url: '/users',
method: 'get',
data
})
}
export function getUser() {
return request({
url: '/index/getUserInfo',
method: 'get'
})
}
export function saveUser(data = {}, id) {
if (id) {
return request({
url: '/user',
method: 'put',
data
})
}
return request({
url: `/user/${id}`,
method: 'put',
data
})
}

View File

@ -0,0 +1,53 @@
<template>
<n-cascader v-bind="getProps" @update:value="handleUpdate" />
</template>
<script>
import { NCascader } from 'naive-ui'
import { computed } from 'vue'
export default {
name: 'AreaCascader',
props: {
...NCascader.props,
showCheckbox: {
type: Boolean,
default: true
},
field: {
type: Array,
default: () => {
return ['province', 'city', 'distance']
}
}
},
emits: ['selectd'],
setup(props, { emit }) {
const getProps = computed(() => {
return {
...props
}
})
function handleUpdate(value, option, pathValues) {
const field = props.field
const valueField = props.valueField
const filedJson = {}
field.forEach((item, index) => {
filedJson[item] = pathValues?.[index]?.[valueField] || ''
})
emit('selectd', filedJson)
}
function clearValue() {
handleUpdate(null, null, [])
}
return {
getProps,
handleUpdate,
clearValue
}
}
}
</script>
<style scoped lang='scss'>
</style>

View File

@ -0,0 +1,22 @@
<template>
<n-breadcrumb>
<n-breadcrumb-item href="#">
北京总行
</n-breadcrumb-item>
<n-breadcrumb-item href="#">
天津分行
</n-breadcrumb-item>
<n-breadcrumb-item href="#">
平山道支行
</n-breadcrumb-item>
</n-breadcrumb>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
components: {
}
})
</script>

View File

@ -0,0 +1,59 @@
<template>
<n-modal
v-bind="getModalOptions"
:style="`width:${getModalOptions.width}px`"
preset="card"
:title="options.title"
@update:show="handleClose"
>
<n-card :bordered="false">
<slot name="Context" />
<n-space style="float: right">
<n-button @click="handleClose">取消</n-button>
<n-button type="primary" @click="handleClick">确认</n-button>
</n-space>
</n-card>
</n-modal>
</template>
<script>
import { defineComponent, computed, unref } from 'vue'
export default defineComponent({
name: 'Modal',
props: {
options: {
type: Object,
default: () => {}
}
},
emits: {
save: null, // click
onClose: (value) => {
return value
}
},
setup(props, { emit }) {
const getModalOptions = computed(() => {
return {
...props.options,
width: props.options.width || 600
}
})
const handleClick = function() {
emit('save')
}
const handleClose = function() {
emit('onClose', true)
}
return {
getModalOptions,
handleClick,
handleClose
}
}
})
</script>
<style scoped lang='scss'>
</style>

View File

@ -0,0 +1,155 @@
<template>
<div class="table-toolbar">
<!--顶部左侧区域-->
<div class="table-toolbar-left">
<slot name="tableTitle" />
</div>
<div class="table-toolbar-right">
<!--顶部右侧区域-->
<slot name="toolbar" />
<!--刷新-->
<n-tooltip trigger="hover">
<template #trigger>
<div class="table-toolbar-right-icon" @click="reload">
<n-icon size="18">
<ReloadOutlined />
</n-icon>
</div>
</template>
<span>刷新</span>
</n-tooltip>
</div>
</div>
<div class="s-table">
<n-data-table
ref="tableElRef"
v-bind="getBindProps"
:pagination="pagination"
@update:page="updatePage"
@update:page-size="updatePageSize"
>
<template #empty>
<slot name="empty" />
</template>
</n-data-table>
</div>
</template>
<script>
import { ReloadOutlined } from '@vicons/antd'
import { tableProps } from './tools/props.js'
import { useDataSource } from './tools/useDataSource.js'
import { usePagination } from './tools/usePagination.js'
import { ref, unref, computed, toRaw, provide } from 'vue'
export default {
name: 'DataTable',
components: { ReloadOutlined },
props: {
...tableProps
},
emits: [
'fetch-success',
'fetch-error',
'update:checked-row-keys',
'edit-end',
'edit-cancel',
'edit-row-end',
'edit-change'
],
setup(props, { emit }) {
const getProps = computed(() => {
return { ...props }
})
/* loading--start */
const loadingRef = ref(unref(getProps).loading)
const getLoading = computed(() => unref(loadingRef))
function setLoading(loading) {
loadingRef.value = loading
}
/* loading--end */
/* pagination-start */
const { getPaginationInfo, setPagination } = usePagination(getProps)
const pagination = computed(() => toRaw(unref(getPaginationInfo)))
/* 页码切换 */
function updatePage(page) {
setPagination({ page: page })
reload()
}
/* 分页数量切换 */
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size })
reload()
}
/* pagination-end */
/* tableData-start */
const tableData = ref([])
const { getDataSourceRef, getRowKey, reload, reFetch } = useDataSource(getProps, { getPaginationInfo, setPagination, tableData, setLoading }, emit)
const isRequest = !!unref(getProps).request
const getBindProps = computed(() => {
return {
...unref(getProps),
loading: unref(getLoading),
rowKey: unref(getRowKey),
data: isRequest ? unref(getDataSourceRef) : unref(getProps).data,
remote: true
}
})
emit('fetch-success', isRequest ? unref(getDataSourceRef) : unref(getProps).data)
const key = Symbol('s-table')
provide(key, { getBindProps })
/* tableData-end */
return {
getBindProps,
pagination,
updatePage,
updatePageSize,
reload,
reFetch
}
}
}
</script>
<style scoped lang='scss'>
.table-toolbar {
display: flex;
justify-content: space-between;
padding: 15px 10px;
.table-toolbar-left {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
}
.table-toolbar-right {
display: flex;
justify-content: flex-end;
align-items: center;
flex: 1;
.table-toolbar-right-icon {
margin-left: 12px;
font-size: 16px;
cursor: pointer;
.n-icon{
vertical-align: middle;
}
&:hover {
color: #1890ff;
}
}
}
}
.table-toolbar-inner-popover-title {
padding: 2px 0;
}
</style>

View File

@ -0,0 +1,86 @@
<template>
<div v-if="getIsShow" class="tableAction" :style="`justify-content: ${getAlign}`">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<n-button
v-if="!action.type || action.type === 'button'"
class="tableAction__item"
v-bind="action.props"
>{{ action.label }}</n-button>
<n-popconfirm
v-if="action.type === 'popconfirm'"
v-bind="action.props"
>
<template #trigger>
<n-button v-bind="action.ButtonProps" class="tableAction__item">{{ action.label }}</n-button>
</template>
{{ action.tip }}
</n-popconfirm>
</template>
</div>
</template>
<script>
import { defineComponent, computed, toRaw, reactive } from 'vue'
export default defineComponent({
name: 'TableAction',
props: {
actions: {
type: Array,
default: null,
required: true
},
align: {
type: String,
default: 'center',
validator: (value) => {
return ['left', 'right', 'center'].indexOf(value) !== -1
}
},
show: {
type: Boolean,
default: true
}
},
setup(props, { emit }) {
const data = reactive({
permissionList: [
'basic_list'
]
})
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
if (!Object.keys(action).includes('show')) {
action.show = Object.keys(action).includes('hidden') ? !action.hidden : true
}
return (data.permissionList.includes(action.auth) || action.auth === '') && action.show
})
})
const getAlign = computed(() => {
return toRaw(props.align)
})
const getIsShow = computed(() => {
return toRaw(props.show)
})
return {
getActions,
getAlign,
getIsShow
}
}
})
</script>
<style scoped lang='scss'>
.tableAction{
display: flex;
align-items: center;
.tableAction__item{
margin: 0 5px;
}
// justify-content: center;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<n-image v-bind="getProps" />
</template>
<script>
import { defineComponent, computed, toRaw } from 'vue'
export default defineComponent({
name: 'TableImage',
props: {
images: {
type: Object,
default: null,
required: true
}
},
setup(props, { emit }) {
const getProps = computed(() => {
return (toRaw(props.images))
})
return {
getProps
}
}
})
</script>
<style scoped lang='scss'>
</style>

View File

@ -0,0 +1,44 @@
<template>
<n-switch v-bind="getSwitchProps" v-model:value="switchVlue" @update:value="changeValue" />
</template>
<script>
import { defineComponent, ref, unref, computed } from 'vue'
export default defineComponent({
name: 'TableSwitch',
props: {
data: {
type: [Object, String, Number, Boolean],
required: true
},
rowKey: {
type: String,
default: ''
}
},
emits: ['change'],
setup(props, { emit }) {
const switchVlue = ref()
const { data, rowKey } = unref(props)
switchVlue.value = rowKey ? data[rowKey] : data
const getSwitchProps = computed(() => {
return {
...unref(props)
}
})
function changeValue(value) {
const { data } = props
const params = {
data,
value
}
emit('change', params)
}
return {
switchVlue,
getSwitchProps,
changeValue
}
}
})
</script>

View File

@ -0,0 +1,99 @@
<template>
<template v-if="isFilter">
<n-tag
v-for="(item,index) in getData.data"
:key="`tag_${index}`"
v-bind="getProps"
:color="getFilter(item[getData.rowKey])?.color || getProps?.color"
>
{{ getFilter(item[getData.rowKey]).label }}
</n-tag>
</template>
<template v-else>
<n-tag v-for="(item,index) in getData.data" :key="`tag_${index}`" v-bind="getProps">
{{ item[getData.rowKey] }}
</n-tag>
</template>
</template>
<script>
import { defineComponent, computed, unref } from 'vue'
import { isArray } from '@/utils/is.js'
export default defineComponent({
name: 'TableTags',
props: {
/* 展示的数据 */
data: {
type: [Array, String, Number],
required: true
},
/* 展示数据取的字段 */
rowKey: {
type: String,
default: 'name'
},
/* 过滤的数据 */
// filters: [
// {
// key: '',
// label: '',
// color: {}
// }
// ],
filters: {
type: Array,
default: null
},
/* tag标签的属性 */
tags: {
type: Object,
default: null
}
},
setup(props, { emit }) {
const isFilter = computed(() => {
return !!(props.filters)
})
const { filters } = unref(props)
function getFilter(value) {
const data = filters.find(item => {
return item.value === value
})
return data || {
value: value,
label: value
}
}
/* 获取传递的数据 */
const getData = computed(() => {
return {
rowKey: unref(props.rowKey),
data: isArray(props.data) ? { ...unref(props.data) } : [{ [props.rowKey]: props.data }],
filters: { ...unref(props.filters) }
}
})
/* 获取tags的属性 */
const getProps = computed(() => {
return {
...unref(props.tags),
closable: false,
bordered: props.tags?.bordered || false
}
})
return {
isFilter,
getFilter,
getData,
getProps
}
}
})
</script>
<style scoped lang='scss'>
.n-tag{
background: transparent;
}
</style>

View File

@ -0,0 +1,49 @@
import { NDataTable } from 'naive-ui'
export const tableProps = {
...NDataTable.props,
/* 初始化接口请求 */
request: {
type: Function,
default: null
},
/* 分页信息 */
pagination: {
type: [Object, Boolean],
default: () => {}
},
/* 数据格式 */
dataType: {
type: String,
default: 'flat',
validator: (value) => {
return ['flat', 'tree'].indexOf(value) !== -1
}
},
/* 分页设置信息 */
paginationSetting: {
type: Object,
default: () => {
return {
// 当前页的字段名
pageField: 'page',
// 每页数量字段名
sizeField: 'limit',
// 接口返回的字段名
listPageField: 'current',
// 接口返回的数据字段名
listField: 'records',
// 接口返回总页数字段名
totalField: 'total',
// 默认分页数量
pageSize: 10,
// 可切换每页数量集合
pageSizes: [10, 20, 30, 40, 50],
// 是否显示每页条数的选择器
showSizePicker: false,
// 是否显示快速跳转
showQuickJumper: false
}
}
}
}

View File

@ -0,0 +1,69 @@
/**
* pid形式数据转children形式
* @param data 需要转换的数组
* @param idKey id字段名
* @param pidKey pid字段名
* @param childKey 生成的children字段名
* @param pid 顶级的pid
* @param addPIds 是否添加所有父级id的字段
* @param parentsKey 所有父级id的字段名称默认parentIds
* @param parentIds 所有父级id
* @returns {[]}
*/
export function toTreeData(data, idKey, pidKey, childKey, pid, addPIds, parentsKey, parentIds) {
if (typeof data === 'object' && !Array.isArray(data)) {
idKey = data.idKey
pidKey = data.pidKey
childKey = data.childKey
pid = data.pid
addPIds = data.addPIds
parentsKey = data.parentsKey
parentIds = data.parentIds
data = data.data
}
if (!childKey) {
childKey = 'children'
}
if (typeof pid === 'undefined') {
pid = []
data.forEach((d) => {
let flag = true
for (let i = 0; i < data.length; i++) {
if (d[pidKey] === data[i][idKey]) {
flag = false
break
}
}
if (flag) {
pid.push(d[pidKey])
}
})
}
const result = []
data.forEach((d) => {
if (d[idKey] === d[pidKey]) {
console.error('data error: ', d)
return
}
if (Array.isArray(pid) ? (pid.indexOf(d[pidKey]) !== -1) : (d[pidKey] === pid)) {
const children = toTreeData({
data: data,
idKey: idKey,
pidKey: pidKey,
childKey: childKey,
pid: d[idKey],
addPIds: addPIds,
parentsKey: parentsKey,
parentIds: (parentIds || []).concat([d[idKey]])
})
if (children.length > 0) {
d[childKey] = children
}
if (addPIds) {
d[parentsKey || 'parentIds'] = parentIds || []
}
result.push(d)
}
})
return result
}

View File

@ -0,0 +1,138 @@
import { ref, unref, computed, onMounted } from 'vue'
import { isBoolean } from '@/utils/is'
import { toTreeData } from './toTree'
export function useDataSource(propsRef, { getPaginationInfo, setPagination, setLoading, tableData }, emit) {
const dataSourceRef = ref([])
const paginationPage = ref(1)
async function fetch(opt) {
try {
/* 设置loading */
setLoading(true)
const { request, pagination, paginationSetting, dataType } = unref(propsRef)
/* 无接口请求中断 */
if (!request) return
/* 获取分页信息 */
const pageField = paginationSetting.pageField
const listPageField = paginationSetting.listPageField
const sizeField = paginationSetting.sizeField
const totalField = paginationSetting.totalField
const listField = paginationSetting.listField
let pageParams = {}
const { page = 1, pageSize = 10 } = unref(getPaginationInfo)
/* 判断是否需要分页信息 */
const noPagination = (isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)
if (noPagination) {
pageParams = {}
} else {
pageParams[pageField] = (opt && opt[pageField]) || page
paginationPage.value = pageParams[pageField]
pageParams[sizeField] = pageSize
}
const params = {
...pageParams
}
const response = await request(params)
const res = noPagination ? response : response.data
const resultTotal = res[totalField] || 0
const currentPage = res[listPageField]
// 如果数据异常,需获取正确的页码再次执行
if (resultTotal) {
if (page > Math.ceil(resultTotal / pageSize)) {
setPagination({
[pageField]: Math.ceil(resultTotal / pageSize)
})
fetch(opt)
}
}
// 处理数据结构
const resultInfo = res[listField] ? res[listField] : res
dataSourceRef.value = dataType === 'tree' ? dealTree(resultInfo.data) : resultInfo
setPagination({
[pageField]: currentPage,
[totalField]: Math.ceil(resultTotal / pageSize),
itemCount: resultTotal
})
/* 更新页码数据 */
if (opt && opt[pageField]) {
setPagination({
[pageField]: opt[pageField] || 1
})
}
emit('fetch-success', {
items: unref(resultInfo),
resultTotal
})
} catch (error) {
console.error(error)
// emit('fetch-error', error)
dataSourceRef.value = []
} finally {
setLoading(false)
}
}
/**
* 递归遍历数据处理成树形结构
* @returns 返回树形结构数据
*/
function dealTree(info) {
const tree = toTreeData(info, 'id', 'pid', 'children')
return tree
}
const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef)
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef)
}
return unref(dataSourceRef)
})
function getDataSource() {
return getDataSourceRef.value
}
function setTableData(values) {
dataSourceRef.value = values
}
const getRowKey = computed(() => {
const { rowKey } = unref(propsRef)
return rowKey || (() => {
return 'key'
})
})
async function reload(opt) {
await fetch(opt)
}
async function reFetch(opt, reload = true) {
const { paginationSetting } = unref(propsRef)
const pageField = paginationSetting.pageField
const sizeField = paginationSetting.sizeField
const pageSize = paginationSetting.pageSize
setPagination({
[pageField]: reload ? 1 : paginationPage.value,
[sizeField]: pageSize
})
await fetch(opt)
}
onMounted(() => {
setTimeout(() => {
fetch()
}, 15)
})
return {
fetch,
getDataSourceRef,
getDataSource,
setTableData,
getRowKey,
reload,
reFetch
}
}

View File

@ -0,0 +1,40 @@
import { computed, unref, ref } from 'vue'
import { isBoolean } from '@/utils/is'
export function usePagination(refProps) {
const configRef = ref({})
const getPaginationInfo = computed(() => {
const { pagination, paginationSetting } = unref(refProps)
/* 判断是否需要展示分页 */
if ((isBoolean(pagination) && !pagination)) {
return false
}
/* 返回配置的分页信息 */
return {
pageSize: paginationSetting.pageSize,
pageSizes: paginationSetting.pageSizes,
showSizePicker: paginationSetting.showSizePicker,
showQuickJumper: paginationSetting.showQuickJumper,
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
pageCount: unref(configRef)[paginationSetting.totalField],
prefix({ itemCount }) {
return `${itemCount}`
}
}
})
function setPagination(info) {
const paginationInfo = unref(getPaginationInfo)
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info
}
}
function getPagination() {
return unref(getPaginationInfo)
}
return { getPaginationInfo, setPagination, getPagination }
}

View File

@ -0,0 +1,54 @@
<template>
<div />
</template>
<script setup>
import { isNullOrUndef } from '@/utils/is'
import { useDialog } from 'naive-ui'
const NDialog = useDialog()
class Dialog {
success(title, option) {
this.showDialog('success', { title, ...option })
}
warning(title, option) {
this.showDialog('warning', { title, ...option })
}
error(title, option) {
this.showDialog('error', { title, ...option })
}
showDialog(type = 'success', option) {
if (isNullOrUndef(option.title)) {
option.showIcon = false
}
NDialog[type]({
positiveText: 'OK',
closable: false,
...option
})
}
confirm(option = {}) {
this.showDialog(option.type || 'error', {
positiveText: '确定',
negativeText: '取消',
onPositiveClick: option.confirm,
onNegativeClick: option.cancel,
onMaskClick: option.cancel,
...option
})
}
}
window['$dialog'] = new Dialog()
Object.freeze(window.$dialog)
Object.defineProperty(window, '$dialog', {
configurable: false,
writable: false
})
</script>

View File

@ -0,0 +1,112 @@
<template>
<n-dropdown
trigger="click"
:options="getOptions"
class="dropdown-icon"
@select="handleSelect"
>
<n-input placeholder="" readonly>
<template #prefix>
<n-icon
v-for="(item,index) in getOptions"
v-show="item.key === selectIcon"
:key="index"
color="#111111"
:component="item.component"
size="24"
/>
</template>
</n-input>
</n-dropdown>
</template>
<script>
import { reactive, toRefs, h, computed } from 'vue'
import { NIcon } from 'naive-ui'
import Icons from './tools/icon.js'
export default {
name: 'IconBoard',
props: {
selected: {
type: String,
default: null
}
},
emits: ['update:selected'],
setup(props, { emit }) {
const data = reactive({
selectIcon: props.selected
})
const renderOption = ({ node, option }) => {
return h(NIcon, {
class: 'board-icon',
size: 24,
onClick: () => {
console.log(option.key)
}
},
{ default: () => h(option.key) })
}
const getOptions = computed(() => {
const options = Icons.map((item, index) => {
const object = {
key: `icon_${item.name}`,
component: item,
icon: function icon() {
return h(NIcon, {
class: 'board-icon',
color: '#111111',
size: 24 }, {
default: () => h(item)
})
}
}
return object
})
return options
})
const nodeProps = (option) => {
return {
class: 'dropdown__icon',
style: {
display: 'inline-block'
}
}
}
function handleSelect(key) {
data.selectIcon = key
emit('update:selected', key)
}
return {
...toRefs(data),
Icons,
renderOption,
getOptions,
nodeProps,
handleSelect
}
}
}
</script>
<style lang='scss'>
.v-binder-follower-content{
.dropdown-icon{
width: 510px;
padding: 5px 15px;
.n-dropdown-option{
display: inline-block;
padding: 6px;
}
.n-dropdown-option-body__suffix{
display: none !important;
}
}
}
</style>

View File

@ -0,0 +1,18 @@
/*
* @Author: whyafterme
* @Date: 2022-06-16 11:39:28
* @LastEditTime: 2022-06-29 13:51:40
* @LastEditors: whyafterme
* @Description:
* @FilePath: \web\src\components\IconBoard\tools\icon.js
*/
import {
EditOutlined, LogoutOutlined
} from '@vicons/antd'
const Icons = [
EditOutlined,
LogoutOutlined
]
export default Icons

View File

@ -0,0 +1,81 @@
<template>
<div>
<n-upload
:default-file-list="fileList"
v-bind="getImgOptions"
:on-change="handleChange"
:on-before-upload="handleBeforeUpload"
>
点击上传
</n-upload>
<n-modal
preset="card"
style="width: 600px"
>
<img :src="previewImageUrl" style="width: 100%">
</n-modal>
</div>
</template>
<script>
import { defineComponent, reactive, toRefs, computed, unref } from 'vue'
import { getToken } from '@/utils/token'
export default defineComponent({
name: 'ImgUpload',
props: {
options: {
type: Object,
default: null
},
size: {
type: Number,
default: null
}
},
setup(props, { emit }) {
const data = reactive({
fileList: []
})
const BaseURL = window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API || import.meta.env.VITE_APP_GLOB_BASE_API
const getImgOptions = computed(() => {
return {
...unref(props.options),
listType: 'image-card',
defaultUpload: props.options.defaultUpload || false,
action: `${BaseURL}${props.options.action}`,
headers: {
'Authorization': getToken(),
...props.options.headers
}
}
})
function handleChange(res) {
console.log('已选择的文件:', res.file.file)
}
/**
* @description: 上传前判断文件是否符合条件
* @param {*} options
* @return {*}
*/
function handleBeforeUpload(options) {
const { file, fileList } = options
if (props.size) {
const size = file.file.size
if (size < props.size * 1024 * 1024) {
fileList.splice(fileList.length - 1)
$message.error('选择的文件大小不满足条件!')
}
}
}
return {
...toRefs(data),
getImgOptions,
handleChange,
handleBeforeUpload
}
}
})
</script>
<style scoped lang='scss'>
</style>

View File

@ -0,0 +1,58 @@
<template>
<VAceEditor
ref="editor"
v-model:value="showText"
style="width:100%;height:150px"
lang="json"
theme="chrome"
@blur="handleInput"
@init="initEditor"
/>
</template>
<script>
import { VAceEditor } from 'vue3-ace-editor'
import { reactive, toRefs } from 'vue'
import 'ace-builds/src-noconflict/mode-json.js'
import 'ace-builds/src-noconflict/theme-chrome.js'
import 'ace-builds/src-noconflict/ext-language_tools.js'
import { isArray } from '@/utils/is.js'
export default {
name: 'JsonEditor',
components: { VAceEditor },
props: {
jsonStr: {
type: [Array, String],
default: () => [{ label: '字段名', value: '字段值' }]
}
},
emits: ['json-change'],
setup(props, { emit }) {
const data = reactive({
options: {
tabSize: 2,
fontSize: 13
},
readOnly: false,
showText: '',
showJson: isArray(props.jsonStr) ? props.jsonStr : JSON.parse(props.jsonStr)
})
const initEditor = () => {
data.showText = JSON.stringify(data.showJson, null, 2)
}
function handleInput() {
emit('json-change', data.showText)
}
return {
...toRefs(data),
initEditor,
handleInput
}
}
}
</script>
<style scoped lang='scss'>
</style>

View File

@ -0,0 +1,17 @@
<template>
<div />
</template>
<script>
export default {
name: 'LoadingBar'
}
</script>
<script setup>
import { useLoadingBar } from 'naive-ui'
window['$loadingBar'] = useLoadingBar()
Object.defineProperty(window, '$loadingBar', {
configurable: false,
writable: false
})
</script>

View File

@ -0,0 +1,65 @@
<template>
<div />
</template>
<script setup>
import { useMessage } from 'naive-ui'
const NMessage = useMessage()
let loadingMessage = null
class Message {
removeMessage(message, duration = 2000) {
setTimeout(() => {
if (message) {
message.destroy()
message = null
}
}, duration)
}
showMessage(type, content, option = {}) {
if (loadingMessage && loadingMessage.type === 'loading') {
loadingMessage.type = type
loadingMessage.content = content
if (type !== 'loading') {
this.removeMessage(loadingMessage, option.duration)
}
} else {
const message = NMessage[type](content, option)
if (type === 'loading') {
loadingMessage = message
}
}
}
loading(content) {
this.showMessage('loading', content, { duration: 0 })
}
success(content, option = {}) {
this.showMessage('success', content, option)
}
error(content, option = {}) {
this.showMessage('error', content, option)
}
info(content, option = {}) {
this.showMessage('info', content, option)
}
warning(content, option = {}) {
this.showMessage('warning', content, option)
}
}
window['$message'] = new Message()
Object.defineProperty(window, '$message', {
configurable: false,
writable: false
})
</script>

View File

@ -0,0 +1,101 @@
<template>
<n-modal
ref="modalRef"
v-bind="getModalOptions"
:style="`width:${getModalOptions.width}px`"
:title="options.title"
>
<n-card :bordered="false">
<slot name="Context" />
</n-card>
</n-modal>
</template>
<script>
import { defineComponent, computed, ref } from 'vue'
export default defineComponent({
name: 'CardDialogModal',
props: {
options: {
type: Object,
default: () => {}
}
},
emits: {
onConfirm: null,
onClose: (value) => {
return value
}
},
setup(props, { emit }) {
const modalRef = ref(null)
const getModalOptions = computed(() => {
return {
...props.options,
width: props.options.width || 600,
preset: props.options.preset || 'dialog',
showIcon: !!props.options.showIcon
}
})
const handleConfirm = function() {
emit('onConfirm')
return false
}
const handleClose = function() {
emit('onClose', true)
}
// setTimeout(() => {
// const dialogHeaderEl = document.querySelector('.n-card-header')
// const dragDom = document.querySelector('.n-modal')
// dragDom.style.overflow = 'auto'
// dialogHeaderEl.style.cursor = 'move'
// const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
// const moveDown = (e) => {
// //
// const disX = e.clientX - dialogHeaderEl.offsetLeft
// const disY = e.clientY - dialogHeaderEl.offsetTop
// // px
// let styL, styT
// // ie 50% px
// if (sty.left.includes('%')) {
// styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
// styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
// } else {
// styL = +sty.left.replace(/\px/g, '')
// styT = +sty.top.replace(/\px/g, '')
// }
// document.onmousemove = function(e) {
// //
// const l = e.clientX - disX
// const t = e.clientY - disY
// //
// dragDom.style.left = `${l + styL}px`
// dragDom.style.top = `${t + styT}px`
// }
// document.onmouseup = function(e) {
// document.onmousemove = null
// document.onmouseup = null
// }
// }
// dialogHeaderEl.onmousedown = moveDown
// })
return {
modalRef,
getModalOptions,
handleConfirm,
handleClose
}
}
})
</script>
<style scoped lang='scss'>
::v-deep(.n-scrollbar-content){
&:first-child{
display: none;
}
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<div>
<n-form ref="formRef" v-bind="getFormOptions">
<template v-for="(item, index) in getFormOptions.info" :key="`${index}-${item.label}`">
<n-form-item :class="{'hidden-item': index > showItemNum}" :label="item.label">
<n-input v-if="['input'].includes(item.type) || !item.type" v-model:value="getFormOptions.form[item.key]" v-bind="item.props" />
<n-select v-if="['select'].includes(item.type)" v-model:value="getFormOptions.form[item.key]" v-bind="item.props" />
<AreaCascader v-if="['area'].includes(item.type)" :ref="el=>{itemRefs[item.refIndex] = el}" v-model:value="getFormOptions.form[item.key]" v-bind="item.props" @selectd="handleSelect" />
<n-date-picker v-if="['date'].includes(item.type)" v-model:formatted-value="getFormOptions.form[item.key]" v-bind="item.props" />
</n-form-item>
</template>
<n-form-item class="form__button">
<n-button type="primary" @click="handleSearch">查询</n-button>
<n-button @click="handleReset">重置</n-button>
<n-button
v-if="showButton"
type="text"
@click="showMoreItem"
>{{ showItemNum === getFormOptions.info.length - 1 ? '收起' : '展开' }}</n-button>
</n-form-item>
</n-form>
</div>
</template>
<script>
import { computed, ref, unref, watch } from 'vue'
import { NForm } from 'naive-ui'
import AreaCascader from '@/components/AreaCascader/index.vue'
export default {
name: 'SearchPage',
components: { AreaCascader },
props: {
...NForm.props,
info: {
type: Array,
default: () => []
}
},
emits: ['search', 'change', 'reset'],
setup(props, { emit }) {
const showItemNum = ref(30)
const len = ref(props.info.length - 1)
const itemRefs = ref([])
const showButton = ref(!!(showItemNum.value < len.value))
const form = ref({})
/* 初始化搜索表单信息 */
function initForm() {
Object.keys(form.value).forEach((key) => {
const index = unref(props).info.findIndex((item) => item.key === key)
form.value[key] = (props).info[index].value || null
})
itemRefs.value.forEach((item) => {
item.clearValue()
})
}
/* 地图选择事件 */
function handleSelect(data) {
form.value = {
...form.value,
...data
}
}
const getFormOptions = computed(() => {
props.info.forEach((item) => {
// const hasInit = item.init || false
// const hasKey = Object.keys(form.value).includes(item.key)
// form.value[item.key] = hasInit ? (item.value || null) : hasKey ? form.value[item.key] : (item.value || null)
form.value[item.key] = item.value || null
})
return {
form: unref(form),
labelWidth: 'auto',
labelPlacement: 'left',
inline: true,
info: [...props.info]
}
})
watch(getFormOptions.value.form,
(value) => {
emit('change', getFormOptions.value.form)
})
function handleSearch() {
emit('search', getFormOptions.value.form)
}
function handleReset() {
initForm()
emit('reset', getFormOptions.value.form)
}
function setFormValue(params) {
Object.keys(params).forEach((key) => {
const index = unref(props).info.findIndex((item) => item.key === key)
form.value[key] = (props).info[index].value || null
})
}
function showMoreItem() {
showItemNum.value = showItemNum.value === len.value ? 3 : len.value
}
return {
showItemNum,
showButton,
getFormOptions,
handleSearch,
handleReset,
setFormValue,
handleSelect,
itemRefs,
showMoreItem
}
}
}
</script>
<style scoped lang='scss'>
.n-form{
flex-wrap: wrap;
}
.n-form-item{
.n-input{
width: 200px;
}
.n-select{
width: 200px;
}
.n-cascader{
width: 200px;
}
&.hidden-item{
display: none;
}
transition: all 10s;
}
.form__button{
button+button{
margin-left: 20px;
}
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div v-if="external" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-bind="$attrs" />
<svg v-else :class="svgClass" aria-hidden="true" v-bind="$attrs">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
import { isExternal } from '@/utils/is.js'
import { defineComponent, computed } from 'vue'
export default defineComponent({
name: 'SvgIcon',
props: {
prefix: {
type: String,
default: 'icon'
},
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const external = computed(() => {
return isExternal(props.iconClass)
})
const iconName = computed(() => {
return `#icon-${props.iconClass}`
})
const svgClass = computed(() => {
if (props.className) {
return 'svg-icon ' + props.className
} else {
return 'svg-icon'
}
})
const styleExternalIcon = computed(() => {
return {
mask: `url(${props.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${props.iconClass}) no-repeat 50% 50%`
}
})
return {
external,
iconName,
svgClass,
styleExternalIcon
}
}
})
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<div class="tinymce-box">
<Editor
id="myedit"
v-model="content"
tag-name="div"
:init="init"
:disabled="disabled"
/>
</div>
</template>
<script>
import Editor from '@tinymce/tinymce-vue'
// node_modulestinymce
import tinymce from 'tinymce/tinymce' // tinymcehidden
import 'tinymce/themes/silver' //
import 'tinymce/icons/default' // icon
//
import 'tinymce/plugins/advlist' //
import 'tinymce/plugins/anchor' //
import 'tinymce/plugins/autolink' //
import 'tinymce/plugins/autoresize' // ,pluginsInitheight
import 'tinymce/plugins/autosave' // 稿
import 'tinymce/plugins/charmap' //
import 'tinymce/plugins/code' //
import 'tinymce/plugins/codesample' //
import 'tinymce/plugins/directionality' //
import 'tinymce/plugins/emoticons' //
import 'tinymce/plugins/fullpage' //
import 'tinymce/plugins/fullscreen' //
import 'tinymce/plugins/help' //
import 'tinymce/plugins/hr' // 线
import 'tinymce/plugins/image' //
import 'tinymce/plugins/importcss' // css
import 'tinymce/plugins/insertdatetime' //
import 'tinymce/plugins/link' //
import 'tinymce/plugins/lists' //
import 'tinymce/plugins/media' //
import 'tinymce/plugins/nonbreaking' //
import 'tinymce/plugins/pagebreak' //
import 'tinymce/plugins/paste' //
import 'tinymce/plugins/preview' //
import 'tinymce/plugins/print' //
import 'tinymce/plugins/quickbars' //
import 'tinymce/plugins/save' //
import 'tinymce/plugins/searchreplace' //
import 'tinymce/plugins/tabfocus' // tab
import 'tinymce/plugins/table' //
import 'tinymce/plugins/template' //
import 'tinymce/plugins/textcolor' //
import 'tinymce/plugins/textpattern' //
import 'tinymce/plugins/toc' //
import 'tinymce/plugins/visualblocks' //
import 'tinymce/plugins/visualchars' //
import 'tinymce/plugins/wordcount' //
import { ref, watch } from 'vue'
export default {
name: 'TinymceEditor',
components: {
Editor
},
props: {
modelValue: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
plugins: {
type: [String, Array],
default:
`print preview searchreplace autolink directionality visualblocks visualchars
fullscreen image link media template code codesample table charmap hr pagebreak
nonbreaking anchor insertdatetime advlist lists wordcount textpattern autosave`
},
toolbar: {
type: [String, Array],
default:
`fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor |
alignleft aligncenter alignright alignjustify outdent indent | styleselect formatselect fontselect fontsizeselect | bullist numlist |
blockquote subscript superscript removeformat | table image media charmap emoticons hr pagebreak insertdatetime print preview |
code selectall | indent2em lineheight formatpainter axupimgs`
},
height: {
type: Number,
default: 600
}
},
emits: { 'update:modelValue': null },
setup(props, { emit }) {
const init = {
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide', //
// skin_url: '/tinymce/skins/ui/oxide-dark',//
content_css: '/tinymce/skins/content/default/content.css',
plugins: props.plugins, //
toolbar: props.toolbar, // false
toolbar_mode: 'sliding',
menubar: 'file edit insert view format table tools', // false-- http://tinymce.ax-z.cn/configure/editor-appearance.php --
menu: {
// file: { title: '', items: 'newdocument' },
// edit: { title: '', items: 'undo redo | cut copy paste pastetext | selectall' },
// insert: { title: '', items: 'link image | hr' },
// view: { title: '', items: 'visualaid' }
// format: {
// title: '',
// items:
// 'bold italic underline strikethrough superscript subscript | formats | removeformat',
// },
// table: { title: '', items: 'inserttable tableprops deletetable | cell row column' },
// tools: { title: '', items: 'spellchecker code' },
},
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px', //
font_formats:
`微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;
宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;
Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;`,
height: props.height, // autoresize
placeholder: '在这里输入文字',
branding: false, // tiny
resize: false, // false-,true-'both'-
statusbar: false, //
elementpath: false, //
content_style: 'img {max-width:100%;}' // css
// content_css: '/tinycontent.css', //csscsscss
}
/* 初始化编辑器 */
tinymce.init
const setEditMode = type => {
tinymce.editors['myedit'].setMode(type) //
}
const content = ref(props.modelValue)
watch(
() => props.modelValue,
(value) => {
console.log(value)
content.value = value
}
)
watch(
() => content.value,
(value) => {
emit('update:modelValue', content.value)
}
)
return {
content,
init,
setEditMode
}
}
}
</script>
<style lang="scss">
.tox,.tox-tinymce-aux{
z-index: 3000 !important;
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<div v-if="isFailed && useEmpty">
<slot name="empty" />
</div>
<div v-else :id="getPlayerId" />
</template>
<script>
import { defineComponent, reactive, ref, computed, toRefs, nextTick } from 'vue'
export default defineComponent({
name: 'VideoPlayer',
props: {
/* 播放器id */
id: {
type: String,
default: () => 'palyer'
},
/* 是有填充空白 */
useEmpty: {
type: Boolean,
default: false
}
},
emits: ['timeUpdate', 'video-status'],
setup(props, { emit }) {
const videoPlayer = Symbol.for(props.id)
const data = reactive({
[videoPlayer]: null,
seekTime: null,
canSeek: true,
isSeek: false,
isFailed: true
})
/* 获取播放器的id */
const getPlayerId = computed(() => {
return props.id
})
/* 播放器组件 */
const toolComponent = Aliplayer.Component({
/* 时间更新事件 */
timeupdate(player, e) {
data.seekTime = player.getCurrentTime()
emit('video-status', data.isSeek ? 'skip' : 'playing')
},
ready(player, e) {
emit('video-status', 'ready')
},
pause(player, e) {
emit('video-status', 'pause')
},
play(player, e) {
emit('video-status', 'play')
},
ended(player, e) {
emit('video-status', 'ended')
},
error(player, e) {
data.isFailed = true
emit('video-status', 'error')
}
})
/* 初始化播放器 */
function init(options) {
if (!options.source) {
data.isFailed = true
} else {
data.isFailed = false
nextTick(() => {
/* 实例化ali播放器 */
const player = new Aliplayer(
{
id: props.id,
width: '500px',
height: '260px',
autoplay: true,
...options,
components: [toolComponent]
},
function(player) { player.mute() }
)
/* 监听开始拖拽事件 */
player.on('startSeek', ({ paramData }) => {
/* 仅变更标识 */
data.isSeek = true
})
/* 监听完成拖拽事件 */
player.on('completeSeek', ({ paramData }) => {
/* 仅变更标识 */
data.isSeek = false
data.seekTime = paramData
/* 是否通知跳转 */
if (data.canSeek) {
emit('video-status', 'skip')
}
})
data[videoPlayer] = player
})
}
}
/* 获取当前播放器的时间 */
function getTime() {
let currentTime = 0
let duration = 0
const seekTime = data.seekTime
if (data[videoPlayer] && !data.isFailed) {
currentTime = data[videoPlayer]?.getCurrentTime()
duration = data[videoPlayer]?.getDuration()
}
return {
currentTime,
duration,
seekTime
}
}
/* 设定播放器播放时间 */
function seekTime(time) {
if (data[videoPlayer] && !data.isFailed) {
data.canSeek = false
data[videoPlayer]?.seek(time)
setTimeout(() => {
data.canSeek = true
}, 100)
}
}
/* 设定播放器开始 */
function playVideo() {
if (data[videoPlayer] && !data.isFailed) {
data[videoPlayer]?.play()
}
}
/* 设定播放器暂停 */
function pauseVideo() {
if (data[videoPlayer] && !data.isFailed) {
data[videoPlayer]?.pause()
}
}
/* 销毁播放器 */
function disposeVideo() {
if (data[videoPlayer] && !data.isFailed) {
data[videoPlayer]?.dispose()
}
data.isFailed = true
}
return {
...toRefs(data),
getPlayerId,
init,
getTime,
seekTime,
playVideo,
pauseVideo,
disposeVideo
}
}
})
</script>
<style>
.prism-player .prism-ErrorMessage .prism-error-operation{
border-bottom: none;
}
.prism-player .prism-ErrorMessage .prism-error-operation a.prism-button.prism-button-refresh{
display: none;
}
.prism-player .prism-ErrorMessage .prism-error-operation a.prism-button.prism-button-orange{
display: none;
}
/* .prism-player .prism-ErrorMessage .prism-detect-info.prism-center{
display: none;
} */
</style>

View File

@ -0,0 +1,99 @@
<template>
<n-layout-header class="layout__header" bordered>
<!-- <div class="header__logo">
<n-image height="18" src="/logo.png" preview-disabled />
</div> -->
<!-- <n-dropdown trigger="hover" :options="options" @select="handleSelect">
<div class="user_msg">
<n-image
class="user_avatar"
:src="userInfo.avatar"
preview-disabled
/>
<span class="user_name">{{ userInfo.realname }}</span>
</div>
</n-dropdown> -->
</n-layout-header>
</template>
<script>
import { defineComponent, reactive, toRefs, computed } from 'vue'
import { useRouter } from 'vue-router'
import { EditOutlined, LogoutOutlined } from '@vicons/antd'
import { renderIcon } from '@/utils'
import { useUserStore } from '@/store/modules/user.js'
import { useSettingStore } from '@/store/modules/setting.js'
export default defineComponent({
name: 'LayoutHeader',
setup() {
const router = useRouter()
const userStore = useUserStore()
const settingStore = useSettingStore()
const data = reactive({
options: [
{
label: '修改密码',
key: 'edit',
icon: renderIcon(EditOutlined)
},
{
label: '退出登录',
key: 'out',
icon: renderIcon(LogoutOutlined)
}
],
userInfo: {
avatar: '',
realname: '管理员'
}
})
const getLogoWidth = computed(() => {
return settingStore.getSidebarSetting.width - 30
})
async function handleSelect(key) {
switch (key) {
case 'out':
await logOut()
}
}
async function logOut() {
const res = await userStore.userLogout()
if (res.code === 0) {
router.replace('/login')
}
}
return {
...toRefs(data),
getLogoWidth,
handleSelect
}
}
})
</script>
<style scoped>
.layout__header {
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.header__logo{
height: 18px;
}
.user_msg {
display: flex;
justify-content: flex-end;
align-items: center;
}
.user_avatar {
width: 30px;
height: 30px;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<Modal
:options="getModalOptions"
:on-positive-click="handleConfirm"
:on-negative-click="handleClose"
:on-close="handleClose"
>
<template #Context>
<n-form
ref="formRef"
:model="form"
label-placement="left"
:rules="rules"
:on-positive-click="handleConfirm"
:on-negative-click="handleClose"
>
<n-form-item
label="旧密码:"
path="oldPassword"
>
<n-input
v-model:value="form.oldPassword"
type="password"
:maxlength="20"
placeholder="请输入旧密码"
/>
</n-form-item>
<n-form-item
label="新密码:"
path="newPassword"
>
<n-input
v-model:value="form.newPassword"
type="password"
:maxlength="20"
placeholder="请输入新密码"
/>
</n-form-item>
<n-form-item
label="确认密码:"
path="configmPassword"
>
<n-input
v-model:value="form.configmPassword"
type="password"
:maxlength="20"
placeholder="请再次输入新密码"
/>
</n-form-item>
</n-form>
</template>
</Modal>
</template>
<script>
import Modal from '../../../../components/Modal/index.vue'
import { updatePwd } from '@/api/home/index.js'
import { reactive, toRefs, computed } from '@vue/reactivity'
export default {
name: 'UpdateModal',
components: { Modal },
props: {
visible: {
type: Boolean,
default: false
}
},
emits: {
'update:visible': null
},
setup(props, { emit }) {
const data = reactive({
form: {
oldPassword: '',
newPassword: '',
configmPassword: ''
},
rules: {
oldPassword: {
required: true,
trigger: ['blur', 'input'],
message: '请输入旧密码'
},
newPassword: {
required: true,
trigger: ['blur', 'input'],
message: '请输入新密码'
},
configmPassword: {
required: true,
trigger: ['blur', 'input'],
message: '请再次输入新密码'
}
}
})
const getModalOptions = computed(() => {
return {
title: '修改密码',
show: props.visible,
negativeText: '取消',
positiveText: '确认'
}
})
/* 关闭弹窗 */
const handleClose = () => {
emit('update:visible', false)
}
return {
...toRefs(data),
getModalOptions,
handleClose
}
},
methods: {
handleConfirm() {
this.$refs.formRef.validate((errors) => {
if (!errors) {
updatePwd(this.form).then(res => {
if (res.code === 0) {
this.handleClose()
$message.success(res.msg)
} else {
$message.error(res.msg)
}
})
}
})
}
}
}
</script>

View File

@ -0,0 +1,108 @@
<template>
<n-dropdown trigger="hover" :options="options" @select="selectKey">
<div class="user_msg">
<n-image
class="user_avatar"
:src="userInfo.avatar"
preview-disabled
/>
<span class="user_name">{{ userInfo.realname }}</span>
</div>
</n-dropdown>
<!-- 修改密码 -->
<update-modal v-if="modalShow" v-modal:visible="modalShow" />
</template>
<script>
import { reactive, toRefs, h } from 'vue'
import { NIcon } from 'naive-ui'
import {
Pencil as EditIcon,
LogOutOutline as LogoutIcon
} from '@vicons/ionicons5'
import { useDialog } from 'naive-ui'
import { loginOut } from '@/api/login/index.js'
import { mapActions, mapState } from 'pinia'
import { useUserStore } from '@/store/modules/user.js'
import UpdateModal from './components/UpdateModal.vue'
export default {
name: 'LogOut',
components: { UpdateModal },
setup() {
const renderIcon = (icon) => {
return () => {
return h(NIcon, null, {
default: () => h(icon)
})
}
}
const data = reactive({
options: [
{
label: '修改密码',
key: 'edit',
icon: renderIcon(EditIcon)
},
{
label: '退出登录',
key: 'out',
icon: renderIcon(LogoutIcon)
}
],
modalShow: false
})
//
const dialog = useDialog()
return {
...toRefs(data),
dialog
}
},
computed: {
...mapState(useUserStore, {
userInfo: 'userInfo'
})
},
methods: {
...mapActions(useUserStore, ['logout']),
selectKey(key) {
console.log(key)
if (key === 'out') {
this.dialog.warning({
title: '提示',
content: '确定要退出登录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
loginOut().then((res) => {
if (res.code === 0) {
this.$router.replace('/login')
this.logout()
$message.success(res.msg)
} else {
$message.error(res.msg)
}
})
}
})
} else if (key === 'edit') {
this.modalShow = true
}
}
}
}
</script>
<style scoped>
.user_msg {
display: flex;
justify-content: flex-end;
align-items: center;
}
.user_avatar {
width: 30px;
height: 30px;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<n-menu
:mode="menuMode"
:value="(currentRoute.title && currentRoute.meta.activeMenu) || currentRoute.title"
:options="getMenuOptions"
@update:value="handleMenuSelect"
/>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { computed, defineProps, toRaw } from 'vue'
import { isExternal } from '@/utils/is.js'
import { usePermissionStore } from '@/store/modules/permission'
const props = defineProps({
menuMode: {
type: String,
default: 'vertical'
}
})
const menuMode = toRaw(props.menuMode)
const router = useRouter()
const { currentRoute } = router
const permissionStore = usePermissionStore()
const getMenuOptions = computed(() => {
return generateOptions(permissionStore.routes, '')
})
function resolvePath(basePath, path) {
if (isExternal(path)) return path
return (
'/' +
[basePath, path]
.filter((path) => !!path && path !== '/')
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
.join('/')
)
}
function generateOptions(routes, basePath) {
// console.log('routes', routes)
const options = []
routes.forEach((route) => {
if (route.title && !route.isHidden) {
const curOption = {
label: (route.meta && route.meta.title) || route.title,
key: route.title,
path: resolvePath(basePath, route.path)
}
if (route.children && route.children.length) {
curOption.children = generateOptions(route.children, resolvePath(basePath, route.path))
}
options.push(curOption)
}
})
return options
}
function handleMenuSelect(key, item) {
if (isExternal(item.path)) {
window.open(item.path)
} else {
router.push(item.path)
}
}
</script>
<style scoped lang='scss'>
</style>

View File

@ -0,0 +1,20 @@
<template>
<n-layout-sider
class="layout_sidebar"
v-bind="getSideOptions"
>
<SideMenu />
</n-layout-sider>
</template>
<script setup>
import { computed, toRaw } from 'vue'
import SideMenu from '@/layout/components/Menu/index.vue'
import { useSettingStore } from '@/store/modules/setting.js'
const settingStore = useSettingStore()
const getSideOptions = computed(() => {
return toRaw(settingStore.getSidebarSetting)
})
</script>

View File

@ -0,0 +1,468 @@
<template>
<div class="tabs-view" :class="{'tabs-view-fix': tagsMenuSetting.fixed,}" :style="getChangeStyle">
<div class="tabs-view-main">
<div ref="navWrap" class="tabs-card" :class="{ 'tabs-card-scrollable': state.scrollable }">
<span class="tabs-card-prev" :class="{ 'tabs-card-prev-hide': !state.scrollable }" @click="scrollPrev">
<n-icon size="16" color="#515a6e">
<LeftOutlined />
</n-icon>
</span>
<span class="tabs-card-next" :class="{ 'tabs-card-next-hide': !state.scrollable }" @click="scrollNext">
<n-icon size="16" color="#515a6e">
<RightOutlined />
</n-icon>
</span>
<div ref="navScroll" class="tabs-card-scroll">
<Draggable :list="tabsList" animation="300" item-key="fullPath" class="flex">
<template #item="{ element }">
<div
:id="`tag${element.fullPath.split('/').join('\/')}`"
class="tabs-card-scroll-item"
:class="{ 'active-item': state.activeKey === element.path }"
@click.stop="goPage(element)"
@contextmenu="handleContextMenu($event, element)"
>
<span>{{ element.meta.title }}</span>
<n-icon v-if="!element.meta.affix" size="14" @click.stop="closeTabItem(element)">
<CloseOutlined />
</n-icon>
</div>
</template>
</Draggable>
</div>
</div>
<div class="tabs-close">
<n-dropdown
trigger="hover"
placement="bottom-end"
:options="tagsMemuOptions"
@select="closeHandleSelect"
>
<div class="tabs-close-btn">
<n-icon size="16" color="#515a6e">
<DownOutlined />
</n-icon>
</div>
</n-dropdown>
</div>
<n-dropdown
:show="state.showDropdown"
:x="state.dropdownX"
:y="state.dropdownY"
placement="bottom-start"
:options="tagsMemuOptions"
@clickoutside="onClickOutside"
@select="closeHandleSelect"
/>
</div>
</div>
</template>
<script setup>
import {
LeftOutlined,
RightOutlined,
CloseOutlined,
DownOutlined,
ReloadOutlined,
ColumnWidthOutlined,
MinusOutlined
} from '@vicons/antd'
import Draggable from 'vuedraggable'
import { reactive, computed, watch, defineProps, ref, unref, provide, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTagsMenuStore } from '@/store/modules/tagsMenu.js'
import { useSettingStore } from '@/store/modules/setting.js'
import { getTags, setTags } from '@/utils/tags.js'
import { isString } from '@/utils/is.js'
import { renderIcon } from '@/utils'
const props = defineProps({
collapsed: {
type: Boolean
},
menuMode: {
type: String,
default: 'sidebar'
}
})
/* 白名单 */
const whiteList = ['Login', 'Redirect', 'NOT_FOUND']
/* 基础页面 */
const baseMenu = '/home'
/* 获取路由器 */
const route = useRoute()
const router = useRouter()
const { push, replace } = router
/* 使用store数据 */
const tagsMenuStore = useTagsMenuStore()
const settingStore = useSettingStore()
const tabsList = computed(() => tagsMenuStore.tabsList)
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)
/* 组装样式 */
const getChangeStyle = computed(() => {
const { collapsed, menuMode } = props
const { cWidth, width } = unref(settingStore.getSidebarSetting)
const { fixed } = unref(settingStore.getTagsMenuSetting)
const lenNum = menuMode === 'header' ? '0px' : collapsed ? `${cWidth}px` : `${width}px`
return {
left: lenNum,
width: `calc(100% - ${!fixed ? '0px' : lenNum})`
}
})
let cacheRoutes = []
const simpleRoute = getSimpleRoute(route)
try {
const routesStr = getTags()
cacheRoutes = routesStr ? JSON.parse(routesStr) : [simpleRoute]
} catch (e) {
cacheRoutes = [simpleRoute]
}
/* 同步路由信息 */
const routes = router.getRoutes()
cacheRoutes.forEach((cacheRoute) => {
const findRoute = routes.find((route) => route.path === cacheRoute.path)
if (route) {
cacheRoute.meta = findRoute.meta || cacheRoute.meta
cacheRoute.name = (findRoute.name || cacheRoute.name)
}
})
tagsMenuStore.initTabs(cacheRoutes)
const state = reactive({
activeKey: route.fullPath,
scrollable: false,
showDropdown: false,
dropdownX: 0,
dropdownY: 0
})
watch(
() => route.fullPath,
(to) => {
// if (whiteList.includes(route.name)) return
state.activeKey = to
tagsMenuStore.addTabs(getSimpleRoute(route))
// updateNavScroll(true)
},
{ immediate: true }
)
/**
* @description:
* @param { Object } route
* @return { Object }
*/
function getSimpleRoute(route) {
const { fullPath, hash, meta, name, params, path, query } = route
return { fullPath, hash, meta, name, params, path, query }
}
/**
* @description: 左侧滚动
* @return {*}
*/
function scrollPrev() {
// const containerWidth = navScroll.value.offsetWidth
// const currentScroll = navScroll.value.scrollLeft
// if (!currentScroll) return
// const scrollLeft = currentScroll > containerWidth ? currentScroll - containerWidth : 0
// scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20)
}
/**
* @description: 右侧滚动
* @return {*}
*/
function scrollNext() {
// const containerWidth = navScroll.value.offsetWidth
// const currentScroll = navScroll.value.scrollLeft
// if (!currentScroll) return
// const scrollLeft = currentScroll > containerWidth ? currentScroll - containerWidth : 0
// scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20)
}
/**
* @description: 页面跳转
* @return {*}
*/
function goPage(e) {
const { fullPath } = e
if (fullPath === route.fullPath) return
state.activeKey = fullPath
go(e, true)
}
/**
* @description: 删除项
* @return {*}
*/
function closeTabItem(e) {
const { fullPath } = e
const routeInfo = tabsList.value.find((item) => item.fullPath === fullPath)
removeTab(routeInfo)
}
/**
* @description:
* @return {*}
*/
const removeTab = (route) => {
if (tabsList.value.length === 1) {
// return message.warning('')
}
// delKeepAliveCompName()
tagsMenuStore.closeCurrentTab(route)
//
if (state.activeKey === route.fullPath) {
const currentRoute = tabsList.value[Math.max(0, tabsList.value.length - 1)]
state.activeKey = currentRoute.fullPath
router.push(currentRoute)
}
// updateNavScroll()
}
// const delKeepAliveCompName = () => {
// if (route.meta.keepAlive) {
// const name = router.currentRoute.value.matched.find((item) => item.name === route.name)
// ?.components?.default.name
// if (name) {
// asyncRouteStore.keepAliveComponents = asyncRouteStore.keepAliveComponents.filter(
// (item) => item != name
// )
// }
// }
// }
/* dropdown--start */
/* 右侧下拉菜单 */
const isCurrent = ref(false)
const tagsMemuOptions = computed(() => {
const isDisabled = unref(tabsList).length <= 1
return [
{
label: '刷新当前',
key: '1',
icon: renderIcon(ReloadOutlined)
},
{
label: `关闭当前`,
key: '2',
disabled: unref(isCurrent) || isDisabled,
icon: renderIcon(CloseOutlined)
},
{
label: '关闭其他',
key: '3',
disabled: isDisabled,
icon: renderIcon(ColumnWidthOutlined)
},
{
label: '关闭全部',
key: '4',
disabled: isDisabled,
icon: renderIcon(MinusOutlined)
}
]
})
/* 操作右侧下拉菜单 */
const closeHandleSelect = (key) => {
switch (key) {
//
case '1':
reloadPage()
break
//
case '2':
removeTab(route)
break
//
case '3':
closeOther(route)
break
//
case '4':
closeAll()
break
}
// updateNavScroll()
state.showDropdown = false
}
/* 刷新页面 */
const reloadPage = () => {
// delKeepAliveCompName()
router.push({
path: '/redirect' + unref(route).fullPath
})
}
/* 注入刷新页面方法 */
provide('reloadPage', reloadPage)
/* 关闭其他 */
const closeOther = (route) => {
tagsMenuStore.closeOtherTabs(route)
state.activeKey = route.fullPath
router.replace(route.fullPath)
// updateNavScroll()
}
/* 关闭全部 */
const closeAll = () => {
tagsMenuStore.closeAllTabs()
router.replace(baseMenu)
// updateNavScroll()
}
/* dropdown--end */
/* contextMenu--start */
/**
* @description: 右键菜单
* @return {*}
*/
function handleContextMenu(e, item) {
e.preventDefault()
isCurrent.value = baseMenu === item.path
state.showDropdown = false
nextTick().then(() => {
state.showDropdown = true
state.dropdownX = e.clientX
state.dropdownY = e.clientY
})
}
function onClickOutside() {
state.showDropdown = false
}
/* contextMenu--end */
/**
* @description: 页面跳转
* @param undefined
* @param undefined
* @return {*}
*/
function go(opt, isReplace = false) {
if (!opt) {
return
}
if (isString(opt)) {
isReplace ? replace(opt).catch(e => console.log(e)) : push(opt).catch(e => console.log(e))
} else {
const o = opt
isReplace ? replace(o).catch(e => console.log(e)) : push(o).catch(e => console.log(e))
}
}
</script>
<style scoped lang='scss'>
.tabs-view {
width: 100%;
padding: 6px 0;
display: flex;
transition: all 0.2s ease-in-out;
background: #f5f7f9;
.tabs-view-main {
height: 32px;
display: flex;
max-width: 100%;
min-width: 100%;
.tabs-card{
-webkit-box-flex: 1;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
position: relative;
&.tabs-card-scrollable {
padding: 0 32px;
overflow: hidden;
}
.tabs-card-prev,
.tabs-card-next {
width: 32px;
text-align: center;
position: absolute;
line-height: 32px;
cursor: pointer;
.n-icon {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
}
}
.tabs-card-scroll {
white-space: nowrap;
overflow: hidden;
.tabs-card-scroll-item {
background: #ffffff;
// color: v-bind('tagsMenuSetting.color');
height: 32px;
padding: 6px 16px 4px;
border-radius: 3px;
margin-right: 6px;
cursor: pointer;
display: inline-block;
position: relative;
flex: 0 0 auto;
span {
float: left;
vertical-align: middle;
}
&:hover {
color: #515a6e;
}
.n-icon {
height: 22px;
width: 21px;
margin-right: -6px;
position: relative;
vertical-align: middle;
text-align: center;
color: #808695;
&:hover {
color: #515a6e !important;
}
svg {
height: 21px;
display: inline-block;
}
}
&.active-item {
color: #2d8cf0;
}
}
}
}
}
.tabs-close {
min-width: 32px;
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
// background: var(--color);
border-radius: 2px;
cursor: pointer;
.tabs-close-btn {
// color: var(--color);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.tabs-view-fix {
position: fixed;
z-index: 5;
padding: 6px 19px 6px 10px;
left: 200px;
}
</style>

View File

@ -0,0 +1,123 @@
<template>
<Modal
:options="getModalOptions"
:on-positive-click="handleConfirm"
:on-negative-click="handleClose"
:on-close="handleClose"
>
<template #Context>
<n-form
ref="formRef"
:model="form"
label-placement="left"
:rules="rules"
:on-positive-click="handleConfirm"
:on-negative-click="handleClose"
>
<n-form-item
label="旧密码:"
path="oldPassword"
>
<n-input
v-model:value="form.oldPassword"
type="password"
:maxlength="20"
placeholder="请输入旧密码"
/>
</n-form-item>
<n-form-item
label="新密码:"
path="newPassword"
>
<n-input
v-model:value="form.newPassword"
type="password"
:maxlength="20"
placeholder="请输入新密码"
/>
</n-form-item>
<n-form-item
label="确认密码:"
path="configmPassword"
>
<n-input
v-model:value="form.configmPassword"
type="password"
:maxlength="20"
placeholder="请再次输入新密码"
/>
</n-form-item>
</n-form>
</template>
</Modal>
</template>
<script>
import Modal from '@/components/Modal/index.vue'
import { updatePwd } from '@/api/home/index.js'
import { reactive, toRefs } from '@vue/reactivity'
export default {
name: 'UpdateModal',
components: { Modal },
props: {
visible: {
type: Boolean,
default: false
}
},
emits: {
'update:visible': null
},
setup(props, { emit }) {
const data = reactive({
form: {
oldPassword: '',
newPassword: '',
configmPassword: ''
},
rules: {
oldPassword: {
required: true,
trigger: ['blur', 'input'],
message: '请输入旧密码'
},
newPassword: {
required: true,
trigger: ['blur', 'input'],
message: '请输入新密码'
},
configmPassword: {
required: true,
trigger: ['blur', 'input'],
message: '请再次输入新密码'
}
}
})
/* 关闭弹窗 */
const handleClose = () => {
emit('update:visible', false)
}
return {
...toRefs(data),
handleClose
}
},
methods: {
handleConfirm() {
this.$refs.formRef.validate((errors) => {
if (!errors) {
updatePwd(this.form).then(res => {
if (res.code === 0) {
this.handleClose()
$message.success(res.msg)
} else {
$message.error(res.msg)
}
})
}
})
}
}
}
</script>

51
src/layout/index.vue Normal file
View File

@ -0,0 +1,51 @@
<template>
<!-- <n-space class="layout" :size="0" vertical>
<n-layout>
<Header />
</n-layout>
<n-layout has-sider>
<SideBar />
<n-layout class="layout__content">
<Bread />
<router-view />
</n-layout>
</n-layout>
</n-space> -->
<n-space class="layout" :size="0" vertical>
<n-layout has-sider>
<SideBar v-if="menuMode === 'sidebar'" />
<n-layout>
<Header />
<Tags v-if="tagsMenuSetting.show" />
<n-layout class="layout__content" :class="{'layout__content--fix': tagsMenuSetting.fixed }">
<router-view />
</n-layout>
</n-layout>
</n-layout>
</n-space>
</template>
<script setup>
// import { reactive } from 'vue'
import Header from './components/Header/index.vue'
import SideBar from './components/Sidebar/index.vue'
import Tags from './components/Tags/index.vue'
import { useSettingStore } from '@/store/modules/setting.js'
import { computed } from 'vue'
import { useUserStore } from '@/store/modules/user.js'
const settingStore = useSettingStore()
const menuMode = computed(() => settingStore.getMenuMode)
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)
const useUser = useUserStore()
function getUserNow() {
useUser.getUserInfo()
}
getUserNow()
</script>
<style lang="scss" scoped>
.layout__content--fix{
padding-top: 44px;
}
</style>

19
src/main.js Normal file
View File

@ -0,0 +1,19 @@
import '@/styles/index.scss'
import 'uno.css'
import { createApp } from 'vue'
import { setupRouter } from '@/router'
import { setupStore } from '@/store'
import App from './App.vue'
function setupApp() {
const app = createApp(App)
setupStore(app)
setupRouter(app)
app.mount('#app')
}
setupApp()

View File

@ -0,0 +1,9 @@
import { createPageLoadingGuard } from './page-loading-guard'
import { createPageTitleGuard } from './page-title-guard'
import { createPermissionGuard } from './permission-guard'
export function setupRouterGuard(router) {
createPageLoadingGuard(router)
createPermissionGuard(router)
createPageTitleGuard(router)
}

View File

@ -0,0 +1,15 @@
export function createPageLoadingGuard(router) {
router.beforeEach(() => {
window.$loadingBar?.start()
})
router.afterEach(() => {
setTimeout(() => {
window.$loadingBar?.finish()
}, 200)
})
router.onError(() => {
window.$loadingBar?.error()
})
}

View File

@ -0,0 +1,12 @@
const baseTitle = import.meta.env.VITE_APP_TITLE
export function createPageTitleGuard(router) {
router.afterEach((to) => {
const pageTitle = to.meta?.title
if (pageTitle) {
document.title = `${pageTitle} | ${baseTitle}`
} else {
document.title = baseTitle
}
})
}

View File

@ -0,0 +1,47 @@
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '@/router/routes'
import { getToken } from '@/utils/token'
const WHITE_LIST = ['/login', '/redirect']
export function createPermissionGuard(router) {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
router.beforeEach(async(to, from, next) => {
// const token = getToken()
const token = true
if (token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
// const hasRoutes = !!permissionStore.permissionRoutes.length
/* 暂无权限和菜单 */
const hasRoutes = true
if (hasRoutes) {
next()
} else {
try {
// await userStore.getUserInfo()
const routes = await permissionStore.generateRoutes()
routes.forEach((item) => {
router.addRoute(item)
})
router.addRoute(NOT_FOUND_ROUTE)
router.addRoute(REDIRECT_ROUTE)
next({ ...to, replace: true })
} catch (error) {
// removeToken()
// $message.error(error)
next({ path: '/login', query: { ...to.query, redirect: to.path }})
}
}
}
} else {
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next({ path: '/login', query: { ...to.query, redirect: to.path }})
}
}
})
}

24
src/router/index.js Normal file
View File

@ -0,0 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router'
import { setupRouterGuard } from './guard'
import { basicRoutes } from './routes'
export const router = createRouter({
history: createWebHistory('/'),
routes: basicRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }),
})
export function resetRouter() {
router.getRoutes().forEach((route) => {
const { name } = route
router.hasRoute(name) && router.removeRoute(name)
})
basicRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
}
export function setupRouter(app) {
app.use(router)
setupRouterGuard(router)
}

View File

@ -0,0 +1,72 @@
import Layout from '@/layout/index.vue'
import Home from '@/views/dashboard/index.vue'
import System from './modules/system.js'
export const basicRoutes = [
{
path: '/login',
title: 'Login',
component: () => import('@/views/login/index.vue'),
isHidden: true,
meta: {
title: '登录页'
}
},
{
path: '/',
title: '控制台',
component: Layout,
redirect: '/home',
meta: {
title: '控制台'
},
children: [
{
path: 'home',
title: 'Home',
component: Home,
meta: {
title: '首页',
affix: true
}
}
]
},
...System
]
export const NOT_FOUND_ROUTE = {
title: 'NOT_FOUND',
path: '/:pathMatch(.*)*',
redirect: '/404',
isHidden: true
}
export const REDIRECT_ROUTE = {
path: '/redirect',
title: 'Redirect',
component: Layout,
meta: {
title: 'Redirect',
hideBreadcrumb: true
},
children: [
{
path: '/redirect/:path(.*)',
name: 'Redirect',
component: () => import('@/views/redirect/index.vue'),
meta: {
title: 'Redirect',
hideBreadcrumb: true
}
}
]
}
const modules = import.meta.globEager('./modules/*.js')
const asyncRoutes = []
Object.keys(modules).forEach((key) => {
asyncRoutes.push(...modules[key].default)
})
export { asyncRoutes }

View File

@ -0,0 +1,5 @@
import Layout from '@/layout/index.vue'
export default [
]

25
src/setting/config.js Normal file
View File

@ -0,0 +1,25 @@
const setting = {
/* 布局模式 vertical / horizontal */
/* layoutMode: 'vertical', */
/* 导航模式 sidebar / header */
menuMode: 'sidebar',
headerSetting: {
isReload: true
},
/* 侧边栏属性 */
sidebarSetting: {
isBorder: true,
isTrigger: false,
mode: 'width',
cWidth: 48,
width: 212,
isScroll: false
},
/* tags */
tagsMenuSetting: {
show: false,
fixed: false,
background: '#f5f7f9'
}
}
export default setting

8
src/store/index.js Normal file
View File

@ -0,0 +1,8 @@
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
const store = createPinia()
store.use(piniaPersist)
export function setupStore(app) {
app.use(store)
}

17
src/store/modules/app.js Normal file
View File

@ -0,0 +1,17 @@
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state() {
return {
themeOverrides: {
common: {
primaryColor: '#316c72',
primaryColorSuppl: '#316c72',
primaryColorHover: '#316c72',
successColorHover: '#316c72',
successColorSuppl: '#316c72'
}
}
}
}
})

View File

@ -0,0 +1,129 @@
import { defineStore } from 'pinia'
import { asyncRoutes, basicRoutes } from '@/router/routes'
import { getMenu } from '@/api/auth/index'
import Layout from '@/layout/index.vue'
import modules from '@/utils/module.js'
/**
* @description:
* @param {*} route
* @param {*} role
* @return {*}
*/
function hasPermission(route, role) {
// const routeRole = route.meta?.role ? route.meta.role : []
// if (!role.length || !routeRole.length) {
// return false
// }
// return role.some((item) => routeRole.includes(item))
return true
}
/**
* @description: 过滤权限路由
* @param {*} routes
* @param {*} role
* @return {*}
*/
function filterAsyncRoutes(routes = [], role) {
const ret = []
routes.forEach((route) => {
if (hasPermission(route, role)) {
const curRoute = {
...route,
children: []
}
if (route.children && route.children.length) {
curRoute.children = filterAsyncRoutes(route.children, role)
} else {
Reflect.deleteProperty(curRoute, 'children')
}
ret.push(curRoute)
}
})
return ret
}
/**
* @description:
* @param {*} routes
* @return {*}
*/
function dataArrayToRoutes(routes) {
const res = []
routes.forEach(item => {
const tmp = { ...item }
// // 如果有component配置
// if (tmp.component) {
// // Layout引入
// if (tmp.component === 'Layout') {
// tmp.component = Layout
// } else {
// const sub_view = tmp.component.replace(/^\/*/g, '')
// const component = `../${sub_view}.vue`
// tmp.component = modules[component]
// }
// if (tmp.children) {
// tmp.children = dataArrayToRoutes(tmp.children)
// }
// }
// 如果pid为0
if (tmp.pid === 0) {
// Layout引入
tmp.component = Layout
if (tmp.children) {
tmp.children = dataArrayToRoutes(tmp.children)
}
} else {
const sub_view = tmp.component.replace(/^\/*/g, '')
const component = `../${sub_view}.vue`
tmp.component = modules[component]
}
tmp.name = tmp.title
tmp.meta = {
...tmp.meta,
title: tmp.meta.title || tmp.title
}
res.push(tmp)
})
return res
}
export const usePermissionStore = defineStore('permission', {
state() {
return {
accessRoutes: []
}
},
getters: {
routes() {
return basicRoutes.concat(this.accessRoutes)
},
permissionRoutes() {
return this.accessRoutes
}
},
actions: {
generateRoutesMock(role = []) {
const accessRoutes = filterAsyncRoutes(asyncRoutes, role)
this.accessRoutes = accessRoutes
return accessRoutes
},
async generateRoutes() {
try {
const res = await getMenu()
if (res.code === 0) {
const result = dataArrayToRoutes(res.data)
console.log(result)
this.accessRoutes = result
return Promise.resolve(result)
} else {
return Promise.reject(res.message)
}
} catch (error) {
console.error(error)
return Promise.reject(error.message)
}
}
}
})

View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import config from '@/setting/config.js'
export const useSettingStore = defineStore({
id: 'project-setting',
state: () => ({
...config
}),
getters: {
getMenuMode() {
return this.menuMode
},
getHeaderSetting() {
return this.headerSetting
},
getSidebarSetting() {
return this.sidebarSetting
},
getTagsMenuSetting() {
return this.tagsMenuSetting
}
},
actions: {
}
})

View File

@ -0,0 +1,56 @@
import { defineStore } from 'pinia'
// 不需要出现在标签页中的路由
const whiteList = ['Redirect', 'login']
// 保留固定路由
function retainAffixRoute(list) {
return list.filter((item) => item?.meta?.affix ?? false)
}
export const useTagsMenuStore = defineStore({
id: 'project-tags-menu',
state: () => ({
tabsList: []
}),
getters: {},
actions: {
initTabs(routes) {
// 初始化标签页
this.tabsList = routes
},
addTabs(route) {
// 添加标签页
if (whiteList.includes(route.name)) return false
const isExists = this.tabsList.some((item) => item.fullPath === route.fullPath)
if (!isExists) {
this.tabsList.push(route)
}
return true
},
closeLeftTabs(route) {
// 关闭左侧
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath)
this.tabsList.splice(0, index)
},
closeRightTabs(route) {
// 关闭右侧
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath)
this.tabsList.splice(index + 1)
},
closeOtherTabs(route) {
// 关闭其他
this.tabsList = this.tabsList.filter((item) => item.fullPath === route.fullPath)
},
closeCurrentTab(route) {
// 关闭当前页
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath)
this.tabsList.splice(index, 1)
},
closeAllTabs() {
// 关闭全部
console.log(retainAffixRoute(this.tabsList))
this.tabsList = retainAffixRoute(this.tabsList)
}
}
})

72
src/store/modules/user.js Normal file
View File

@ -0,0 +1,72 @@
import { defineStore } from 'pinia'
import { userLogin, loginOut } from '@/api/login'
import { getUser } from '@/api/login/index.js'
import { setToken, removeToken } from '@/utils/token'
import { useTagsMenuStore } from './tagsMenu.js'
import { usePermissionStore } from './permission.js'
export const useUserStore = defineStore('user', {
persist: true,
state() {
return {
userInfo: {}
}
},
getters: {
userInfoMsg() {
return this.userInfo
}
},
actions: {
/* 登录 */
async getLoginToken(form) {
try {
const res = await userLogin(form)
if (res.code === 0) {
/* 设置token */
setToken(res.data.access_token)
this.getUserInfo()
return Promise.resolve(res)
} else {
return Promise.reject(res)
}
} catch (error) {
return Promise.reject(error)
}
},
/* 获取用户信息 */
async getUserInfo() {
const res = await getUser()
if (res.code === 0) {
this.setUserInfo(res.data)
}
},
async userLogout() {
try {
const res = await loginOut()
if (res.code === 0) {
removeToken()
this.reset()
return Promise.resolve(res)
} else {
return Promise.reject(res)
}
} catch (error) {
return Promise.reject(error)
}
},
reset() {
this.$reset()
const tagsMenuStore = useTagsMenuStore()
const permissionStore = usePermissionStore()
tagsMenuStore.$reset()
permissionStore.$reset()
},
setUserInfo(userInfo = {}) {
this.userInfo = { ...this.userInfo, ...userInfo }
}
}
})

3
src/styles/index.scss Normal file
View File

@ -0,0 +1,3 @@
@import './reset.scss';
@import './public.scss';
@import './layout.scss';

18
src/styles/layout.scss Normal file
View File

@ -0,0 +1,18 @@
.layout,.n-space{
height: inherit;
}
.layout{
.layout__header{
height: 48px;
}
.layout__content{
height: calc(100vh - 48px);
}
}
.n-data-table .n-data-table-tr{
th{
white-space:nowrap;
}
}

56
src/styles/public.scss Normal file
View File

@ -0,0 +1,56 @@
html {
font-size: 4px; // * 1rem = 4px 方便unocss计算在unocss中 1字体单位 = 0.25rem相当于 1等份 = 1px
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #f2f2f2;
font-family: 'Encode Sans Condensed', sans-serif;
}
/* router view transition fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 滚动条凹槽的颜色,还可以设置边框属性 */
*::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
/* 滚动条的宽度 */
*::-webkit-scrollbar {
width: 9px;
height: 9px;
}
/* 滚动条的设置 */
*::-webkit-scrollbar-thumb {
background-color: #ddd;
background-clip: padding-box;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
/* 滚动条鼠标移上去 */
*::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}

40
src/styles/reset.scss Normal file
View File

@ -0,0 +1,40 @@
html {
box-sizing: border-box;
}
*,
::before,
::after {
margin: 0;
padding: 0;
box-sizing: inherit;
}
a {
text-decoration: none;
color: #333;
}
a:hover,
a:link,
a:visited,
a:active {
text-decoration: none;
}
ol,
ul {
list-style: none;
}
input,
textarea {
outline: none;
border: none;
resize: none;
}
body {
font-size: 14px;
font-weight: 400;
}

View File

@ -0,0 +1,5 @@
$primaryColor: #316c72;
:root {
--vh100: 100vh;
}

9
src/utils/cache/index.js vendored Normal file
View File

@ -0,0 +1,9 @@
import { createWebStorage } from './web-storage'
export const createLocalStorage = function(option = {}) {
return createWebStorage({ prefixKey: option.prefixKey || '', storage: localStorage })
}
export const createSessionStorage = function(option = {}) {
return createWebStorage({ prefixKey: option.prefixKey || '', storage: sessionStorage })
}

55
src/utils/cache/web-storage.js vendored Normal file
View File

@ -0,0 +1,55 @@
import { isNullOrUndef } from '@/utils/is'
class WebStorage {
constructor(option) {
this.storage = option.storage
this.prefixKey = option.prefixKey
}
getKey(key) {
return `${this.prefixKey}${key}`.toUpperCase()
}
set(key, value, expire) {
const stringData = JSON.stringify({
value,
time: Date.now(),
expire: !isNullOrUndef(expire) ? new Date().getTime() + expire * 1000 : null
})
this.storage.setItem(this.getKey(key), stringData)
}
get(key) {
const { value } = this.getItem(key, {})
return value
}
getItem(key, def = null) {
const val = this.storage.getItem(this.getKey(key))
if (!val) return def
try {
const data = JSON.parse(val)
const { value, time, expire } = data
if (isNullOrUndef(expire) || expire > new Date().getTime()) {
return { value, time }
}
this.remove(key)
return def
} catch (error) {
this.remove(key)
return def
}
}
remove(key) {
this.storage.removeItem(this.getKey(key))
}
clear() {
this.storage.clear()
}
}
export function createWebStorage({ prefixKey = '', storage = sessionStorage }) {
return new WebStorage({ prefixKey, storage })
}

30
src/utils/dictionary.js Normal file
View File

@ -0,0 +1,30 @@
export const QUESTION_STATUS = [
{ label: '已确认', value: 1 },
{ label: '已忽略', value: 2 },
{ label: '待确认', value: 3 }
]
export const USER_STATUS = [
{ label: '正常', value: 1 },
{ label: '禁用', value: 2 }
]
export const MENU_TYPE = [
{ label: '菜单', value: 0 },
{ label: '按钮', value: 1 }
]
export const MENU_OPEN = [
{ label: '内部', value: '1' },
{ label: '外部', value: '2' }
]
export const MENU_VISIBLE = [
{ label: '可见', value: 0 },
{ label: '不可见', value: 1 }
]
export const MENU_STATUS = [
{ label: '在用', value: 1 },
{ label: '停用', value: 2 }
]

132
src/utils/handleData.js Normal file
View File

@ -0,0 +1,132 @@
/**
* pid形式数据转children形式
* @param data 需要转换的数组
* @param idKey id字段名
* @param pidKey pid字段名
* @param childKey 生成的children字段名
* @param pid 顶级的pid
* @param addPIds 是否添加所有父级id的字段
* @param parentsKey 所有父级id的字段名称默认parentIds
* @param parentIds 所有父级id
* @returns {[]}
*/
export function toTreeData(data, idKey, pidKey, childKey, pid, addPIds, parentsKey, parentIds) {
if (typeof data === 'object' && !Array.isArray(data)) {
idKey = data.idKey
pidKey = data.pidKey
childKey = data.childKey
pid = data.pid
addPIds = data.addPIds
parentsKey = data.parentsKey
parentIds = data.parentIds
data = data.data
}
if (!childKey) {
childKey = 'children'
}
if (typeof pid === 'undefined') {
pid = []
data.forEach((d) => {
let flag = true
for (let i = 0; i < data.length; i++) {
if (d[pidKey] === data[i][idKey]) {
flag = false
break
}
}
if (flag) {
pid.push(d[pidKey])
}
})
}
const result = []
data.forEach((d) => {
if (d[idKey] === d[pidKey]) {
console.error('data error: ', d)
return
}
if (Array.isArray(pid) ? (pid.indexOf(d[pidKey]) !== -1) : (d[pidKey] === pid)) {
const children = toTreeData({
data: data,
idKey: idKey,
pidKey: pidKey,
childKey: childKey,
pid: d[idKey],
addPIds: addPIds,
parentsKey: parentsKey,
parentIds: (parentIds || []).concat([d[idKey]])
})
if (children.length > 0) {
d[childKey] = children
}
if (addPIds) {
d[parentsKey || 'parentIds'] = parentIds || []
}
result.push(d)
}
})
return result
}
/**
* 遍历children形式数据
* @param data 需要遍历的数组
* @param callback 回调
* @param childKey children字段名
*/
export function eachTreeData(data, callback, childKey = 'children') {
if (!data || !data.length) {
return
}
data.forEach((d) => {
if (callback(d) !== false && d[childKey]) {
eachTreeData(d[childKey], callback, childKey)
}
})
}
/**
* 处理树形数据
* @param data 需要处理的数据
* @param formatter 处理器
* @param childKey children字段名
* @returns {[]} 处理后的数据
*/
export function formatTreeData(data, formatter, childKey = 'children') {
const result = []
if (data && data.length) {
data.forEach((d) => {
const item = formatter(d)
if (item !== false) {
if (item[childKey]) {
item[childKey] = formatTreeData(item[childKey], formatter, childKey)
}
result.push(item)
}
})
}
return result
}
/**
* 处理select数据
* @param data 需要处理的数据
* @params {label: XX, value: XX, children: XX} 需要处理的结构
* @returns {[]} 处理后的数据
*/
export function dataToSelect(data, optionsObj) {
const result = []
if (data && data.length) {
data.forEach((item) => {
const i = {}
i.label = item[optionsObj.label]
i.value = item[optionsObj.value]
if (item.children && item.children.length) {
dataToSelect(item.children, optionsObj)
i.children = item[optionsObj.children]
}
result.push(i)
})
}
return result
}

13
src/utils/http/help.js Normal file
View File

@ -0,0 +1,13 @@
import { useUserStore } from '@/store/modules/user'
const WITHOUT_TOKEN_API = [{ url: '/login/login', method: 'POST' }, { url: '/login/captcha', method: 'GET' }]
export function isWithoutToken({ url, method = '' }) {
return WITHOUT_TOKEN_API.some((item) => item.url === url && item.method === method.toUpperCase())
}
export function addBaseParams(params) {
if (!params.userId) {
params.userId = useUserStore().userId
}
}

16
src/utils/http/index.js Normal file
View File

@ -0,0 +1,16 @@
import axios from 'axios'
import { setupInterceptor } from './interceptors'
const mockBaseURL = window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API_MOCK || import.meta.env.VITE_APP_GLOB_BASE_API_MOCK
const defBaseURL = window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API || import.meta.env.VITE_APP_GLOB_BASE_API
const baseURL = import.meta.env.VITE_APP_USE_MOCK === 'true' ? mockBaseURL : defBaseURL
function createAxios(option = {}) {
const service = axios.create({
timeout: option.timeout || 120000,
baseURL: (option.baseURL || defBaseURL) + import.meta.env.VITE_SERVER
})
setupInterceptor(service)
return service
}
export const defAxios = createAxios({ baseURL })

View File

@ -0,0 +1,67 @@
import { router } from '@/router'
import { getToken, removeToken } from '@/utils/token'
import { isWithoutToken } from './help'
export function setupInterceptor(service) {
service.interceptors.request.use(
async(config) => {
// 防止缓存给get请求加上时间戳
if (config.method === 'get') {
config.params = { ...config.params, t: new Date().getTime() }
}
// 处理不需要token的请求
if (isWithoutToken(config)) {
return config
}
// const token = getToken()
const token = 'token'
if (token) {
config.headers.Authorization = token
return config
}
/**
* * 未登录或者token过期的情况下
* * 跳转登录页重新登录携带当前路由及参数登录成功会回到原来的页面
*/
const { currentRoute } = router
router.replace({
path: '/login',
query: { ...currentRoute.query, redirect: currentRoute.path }
})
return Promise.reject({ code: '-1', message: '未登录' })
},
(error) => Promise.reject(error)
)
service.interceptors.response.use(
(response) => {
const { method } = response?.config
const { code } = response?.data
const { currentRoute } = router
switch (code) {
case 0:
if (method !== 'get') {
$message.success(response.data.msg)
}
break
case -1:
$message.error(response.data.msg)
break
case 401:
// 未登录可能是token过期或者无效了
removeToken()
router.replace({
path: '/login',
query: { ...currentRoute.query, redirect: currentRoute.path }
})
break
default:
break
}
return response?.data
},
(error) => {
return Promise.reject(error)
}
)
}

86
src/utils/index.js Normal file
View File

@ -0,0 +1,86 @@
import { h } from 'vue'
import { NIcon } from 'naive-ui'
import dayjs from 'dayjs'
/**
* @desc 格式化时间
* @param {(Object|string|number)} time
* @param {string} format
* @returns {string | null}
*/
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
return dayjs(time).format(format)
}
export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
return formatDateTime(date, format)
}
/**
* @desc 函数节流
* @param {Function} fn
* @param {Number} wait
* @returns {Function}
*/
export function throttle(fn, wait) {
var context, args
var previous = 0
return function() {
var now = +new Date()
context = this
args = arguments
if (now - previous > wait) {
fn.apply(context, args)
previous = now
}
}
}
/**
* @desc 函数防抖
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
export function debounce(method, wait, immediate) {
let timeout
return function(...args) {
const context = this
if (timeout) {
clearTimeout(timeout)
}
// 立即执行需要两个条件一是immediate为true二是timeout未被赋值或被置为null
if (immediate) {
/**
* 如果定时器不存在则立即执行并设置一个定时器wait毫秒后将定时器置为null
* 这样确保立即执行后wait毫秒内不会被再次触发
*/
const callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
method.apply(context, args)
}
} else {
// 如果immediate为false则函数wait毫秒后执行
timeout = setTimeout(() => {
/**
* args是一个类数组对象所以使用fn.apply
* 也可写作method.call(context, ...args)
*/
method.apply(context, args)
}, wait)
}
}
}
/**
* @description: 渲染图标
* @return {*}
*/
export function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) })
}

110
src/utils/is.js Normal file
View File

@ -0,0 +1,110 @@
const toString = Object.prototype.toString
export function is(val, type) {
return toString.call(val) === `[object ${type}]`
}
export function isDef(val) {
return typeof val !== 'undefined'
}
export function isUndef(val) {
return typeof val === 'undefined'
}
export function isNull(val) {
return val === null
}
export function isObject(val) {
return !isNull(val) && is(val, 'Object')
}
export function isArray(val) {
return val && Array.isArray(val)
}
export function isString(val) {
return is(val, 'String')
}
export function isNumber(val) {
return is(val, 'Number')
}
export function isBoolean(val) {
return is(val, 'Boolean')
}
export function isDate(val) {
return is(val, 'Date')
}
export function isRegExp(val) {
return is(val, 'RegExp')
}
export function isFunction(val) {
return typeof val === 'function'
}
export function isPromise(val) {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
export function isElement(val) {
return isObject(val) && !!val.tagName
}
export function isWindow(val) {
return typeof window !== 'undefined' && isDef(window) && is(val, 'Window')
}
export function isNullOrUndef(val) {
return isNull(val) || isUndef(val)
}
export function isEmpty(val) {
if (isArray(val) || isString(val)) {
return val.length === 0
}
if (val instanceof Map || val instanceof Set) {
return val.size === 0
}
if (isObject(val)) {
return Object.keys(val).length === 0
}
return false
}
/**
* * 类似sql的isnull函数
* * 第一个参数为null/undefined/''则返回第二个参数作为默认值否则返回第一个参数
* @param {Number|Boolean|String} val
* @param {Number|Boolean|String} replaceVal
* @returns
*/
export function isNullReplace(val, replaceVal = '') {
return isNullOrUndef(val) || val === '' ? replaceVal : val
}
export function isUrl(path) {
const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
export const isServer = typeof window === 'undefined'
export const isClient = !isServer

17
src/utils/module.js Normal file
View File

@ -0,0 +1,17 @@
// const module = import.meta.glob(`@/views/**/*.vue`)
// const modules = {}
// Object.keys(module).forEach((item) => {
// module[item]().then(res => {
// const moduleName = item.replace(/^\.\/modules\/(.*)\.\w+$/, '$1')
// modules[moduleName] = res?.default
// })
// })
const modulesEager = import.meta.globEager(`@/views/**/*.vue`)
const modules = Object.keys(modulesEager).reduce((modules, path) => {
const moduleName = path.replace(/^\.\/modules\/(.*)\.\w+$/, '$1')
modules[moduleName] = modulesEager[path]?.default
return modules
}, {})
export default modules

18
src/utils/tags.js Normal file
View File

@ -0,0 +1,18 @@
import { createLocalStorage } from './cache'
const STORAGE_CODE = 'tags_menu'
const DURATION = 24 * 60 * 60
export const lsToken = createLocalStorage()
export function getTags() {
return lsToken.get(STORAGE_CODE)
}
export function setTags(token) {
lsToken.set(STORAGE_CODE, token, DURATION)
}
export function removeTags() {
lsToken.remove(STORAGE_CODE)
}

41
src/utils/token.js Normal file
View File

@ -0,0 +1,41 @@
import { createLocalStorage } from './cache'
// import { refreshToken } from '@/api/auth'
const TOKEN_CODE = 'access_token'
const DURATION = 24 * 60 * 60
export const lsToken = createLocalStorage()
/* 获取token */
export function getToken() {
return lsToken.get(TOKEN_CODE)
}
/* 设置token */
export function setToken(token) {
lsToken.set(TOKEN_CODE, token, DURATION)
}
/* 移出token */
export function removeToken() {
lsToken.remove(TOKEN_CODE)
}
/* 刷新token */
// export async function refreshAccessToken() {
// const tokenItem = lsToken.getItem(TOKEN_CODE)
// if (!tokenItem) {
// return
// }
// const { time } = tokenItem
// if (new Date().getTime() - time > 1000 * 60 * 30) {
// try {
// const res = await refreshToken()
// if (res.code === 0) {
// setToken(res.data.token)
// }
// } catch (error) {
// console.error(error)
// }
// }
// }

12
src/utils/ui/theme.js Normal file
View File

@ -0,0 +1,12 @@
const common = {
primaryColor: '#36ad6a'
}
const themeOverrides = {
common,
Button: {
textColor: common.primaryColor
}
}
export default themeOverrides

View File

@ -0,0 +1,25 @@
<template>
<div>
首页
</div>
</template>
<script>
import { useRouter } from 'vue-router'
export default {
name: 'HomePage',
setup(props) {
const router = useRouter()
function toSystem() {
router.push({ path: '/login' })
}
return {
toSystem
}
}
}
</script>
<style lang="scss" scoped>
</style>

125
src/views/login/index.vue Normal file
View File

@ -0,0 +1,125 @@
<template>
<div class="login_bg">
<n-form
ref="formRef"
:model="loginForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
:style="{
maxWidth: '640px',
backgroundColor: '#fff',
padding: '20px',
borderRadius: '10px'
}"
@keyup.enter="handleLogin"
>
<n-form-item label="用户名" path="username">
<n-input v-model:value="loginForm.username" placeholder="请输入用户名" />
</n-form-item>
<n-form-item label="密码" path="password">
<n-input v-model:value="loginForm.password" type="password" placeholder="请输入用户名" />
</n-form-item>
<n-form-item label="验证码" path="captcha">
<n-input v-model:value="loginForm.captcha" placeholder="请输入验证码" />
<img v-if="captcha" :src="captcha" alt="" @click="changeCode">
</n-form-item>
<n-form-item path="remember">
<n-checkbox v-model:checked="loginForm.remember" size="medium" label="记住密码" />
</n-form-item>
<n-form-item>
<n-button @click="handleLogin">登录</n-button>
</n-form-item>
</n-form>
</div>
</template>
<script>
import { userLogin, userCaptcha } from '@/api/login/index.js'
import { setToken } from '@/utils/token'
import { ref, reactive, onMounted } from 'vue'
export default {
name: 'LoginPage',
setup() {
const loginForm = reactive({
username: '',
password: '',
key: '',
captcha: '',
remember: false
})
onMounted(() => {
changeCode(loginForm)
})
//
const captcha = ref('')
async function changeCode(form) {
const params = {
username: form.username,
password: form.password,
captcha: form.captcha
}
const captForm = await userCaptcha(params)
captcha.value = captForm.data.captcha
loginForm.key = captForm.data.key
}
return {
loginForm,
captcha,
changeCode,
rules: reactive({
username: {
required: true,
trigger: ['blur', 'input'],
message: '请输入用户名'
},
password: {
required: true,
trigger: ['blur', 'input'],
message: '请输入密码'
},
captcha: {
required: true,
trigger: ['blur', 'input'],
message: '请输入验证码'
}
})
}
},
methods: {
handleLogin() {
console.log(this.loginForm)
userLogin(this.loginForm).then((res) => {
if (res.code === 0) {
// token
setToken(res.data.access_token)
this.goPage()
} else {
console.log(res)
}
})
},
//
goPage() {
// const query = this.$route.query
// const path = query && query.from ? query.from : '/'
// console.log(path)
// this.$router.replace(path)
this.$router.replace('/home')
}
}
}
</script>
<style lang="scss" scoped>
.login_bg {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -0,0 +1,20 @@
<script>
import { defineComponent, onBeforeMount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
name: 'RedirectPage',
setup() {
const route = useRoute()
const router = useRouter()
onBeforeMount(() => {
const { params, query } = route
const { path } = params
router.replace({
path: '/' + (Array.isArray(path) ? path.join('/') : path),
query
})
})
}
})
</script>

49
vite.config.js Normal file
View File

@ -0,0 +1,49 @@
import { defineConfig, loadEnv } from 'vite'
import { resolve } from 'path'
import { wrapperEnv } from './build/utils'
import { createVitePlugins } from './build/vite/plugin'
import { createProxy } from './build/vite/proxy'
import { OUTPUT_DIR } from './build/constant'
function pathResolve(dir) {
return resolve(__dirname, '.', dir)
}
export default defineConfig(({ command, mode }) => {
const root = process.cwd()
const isBuild = command === 'build'
const env = loadEnv(mode, process.cwd())
const viteEnv = wrapperEnv(env)
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY } = viteEnv
return {
root,
base: VITE_PUBLIC_PATH || '/',
plugins: createVitePlugins(viteEnv, isBuild),
lintOnSave: false,
resolve: {
alias: {
'@': pathResolve('src')
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import '@/styles/variables.scss';`
}
}
},
server: {
host: '0.0.0.0',
port: VITE_PORT,
proxy: createProxy(VITE_PROXY)
},
build: {
target: 'es2015',
outDir: OUTPUT_DIR,
brotliSize: false,
chunkSizeWarningLimit: 2000
}
}
})