Commit 8b532d81 authored by hucy's avatar hucy

fix:提交

parent 49f11459
......@@ -3,14 +3,10 @@
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": [
"source.fixAll.eslint"
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"vue"
],
"typescript.tsdk": "node_modules/typescript/lib"
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"typescript.tsdk": "node_modules/typescript/lib",
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
}
}
......@@ -7,7 +7,6 @@
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { configure } = require('quasar/wrappers');
const path = require('path');
......@@ -28,7 +27,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ['i18n', 'axios'],
boot: ['i18n', 'axios', 'ckeditor5'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'],
......@@ -71,7 +70,12 @@ module.exports = configure(function (/* ctx */) {
// polyfillModulePreload: true,
// distDir
// extendViteConf (viteConf) {},
extendViteConf(viteConf, { isClient, isServer }) {
// do stuff in-place with viteConf
viteConf.esbuild = {
logOverride: { 'this-is-undefined-in-esm': 'silent' },
};
},
// viteVuePluginOptions: {},
vitePlugins: [
......
import { boot } from 'quasar/wrappers';
import CKEditor from '@ckeditor/ckeditor5-vue';
export default boot(({ app }) => {
app.use(CKEditor);
});
......@@ -13,3 +13,5 @@
* `SessionStorage` or `LocalStorage`
*/
export const LANG_STORAGE_KEY = 'LANG';
export const USER_INFO_STORAGE_KEY = 'USER_INFO_STORAGE_KEY';
export * from './use-message';
export * from './use-webstorage';
export * from './use-pagestore';
export * from './use-userinfo';
import { defineStore } from 'pinia';
import { USER_INFO_STORAGE_KEY } from '../constants';
import { useSessionStorage } from './use-webstorage';
interface IUserInfo {
id: number;
username: string;
is_admin: boolean;
}
function getStorageUserInfo(): IUserInfo {
let result: IUserInfo = {
id: -1,
username: '',
is_admin: false,
};
const value = useSessionStorage().getItem(USER_INFO_STORAGE_KEY) as string;
if (value) {
result = JSON.parse(value);
}
return result;
}
const useUserInfoStore = defineStore(USER_INFO_STORAGE_KEY, {
state: () => {
return getStorageUserInfo();
},
actions: {
setUserInfo: function (val: IUserInfo) {
this.$state = val;
useSessionStorage().setItem(USER_INFO_STORAGE_KEY, JSON.stringify(val));
},
},
});
export { useUserInfoStore };
import { isEmpty } from './isEmpty';
/**
* 删除对象中为空的属性值 返回一个新对象
* @param data - 原始对象
**/
export const delEmptyObjkey = function (data: any) {
const obj = {} as any;
for (const key in data) {
if (!isEmpty(data[key])) {
obj[key] = data[key];
}
}
return obj;
};
......@@ -2,8 +2,6 @@ export * from './del-arr-obj';
export * from './find-arr-objs';
export * from './no-repeat-obj-in-arr';
export * from './obj-del';
export * from './is-obj-equal';
export * from './del-empty-objkey';
export * from './json-str';
export * from './isEmpty';
export * from './maths';
......@@ -12,6 +10,9 @@ export * from './get-type';
export * from './is';
export * from './get-angle';
export * from './scale-polygon';
export * from './isBasicData';
export * from './isEqual';
export * from './omitEmpty';
export {
cloneDeep,
orderBy,
......
/**
* 判断两个对象是否相等
**/
export const isObjEqual = function (obj1: any, obj2: any) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length === keys2.length) {
const sortKeys1 = keys1.sort();
const sortKeys2 = keys2.sort();
const sortStr1 = JSON.stringify(sortKeys1);
const sortStr2 = JSON.stringify(sortKeys2);
if (sortStr1 === sortStr2) {
let flag = true;
for (const key1 in obj1) {
if (obj1[key1] !== obj2[key1]) {
flag = false;
break;
}
}
return flag;
} else {
return false;
}
} else {
return false;
}
};
/**
* @description 是否是基本数据类型
* @param {any} data
* @return {boolean}
*/
export function isBasicData(data: any) {
const basicDataTypes = [
'[object String]',
'[object Number]',
'[object Boolean]',
'[object Null]',
'[object Undefined]',
];
const typeofs = Object.prototype.toString.call(data);
return basicDataTypes.includes(typeofs);
}
import { isEmpty } from './isEmpty';
type IndexKey = string | number | symbol;
interface AnyObject {
[propName: IndexKey]: any;
}
/**
* @description 判断两个对象是否相等
* @param {AnyObject} obj1
* @param {AnyObject} obj2
* @param {boolean} omitEmpty 默认为false,当为true时,为空的值判断为相等
* @return {boolean}
*/
export const isObjectEqual = function (
obj1: AnyObject,
obj2: AnyObject,
omitEmpty = false
) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length === keys2.length) {
const sortKeys1 = keys1.sort();
const sortKeys2 = keys2.sort();
const sortStr1 = JSON.stringify(sortKeys1);
const sortStr2 = JSON.stringify(sortKeys2);
// 对象的key值都相等
if (sortStr1 === sortStr2) {
let flag = true;
for (const key1 in obj1) {
const value1 = obj1[key1];
const value2 = obj2[key1];
const typeofs = Object.prototype.toString.call(value1);
const typeofs2 = Object.prototype.toString.call(value2);
// 如果两个都为空,判断为相等
if (omitEmpty && isEmpty(value1) && isEmpty(value2)) {
// flag = true;
} else {
if (typeofs === '[object Object]' && typeofs2 === '[object Object]') {
const _flag = isObjectEqual(value1, value2, omitEmpty);
if (!_flag) {
flag = false;
break;
}
} else if (
typeofs === '[object Array]' &&
typeofs2 === '[object Array]'
) {
const _flag = isArrayEqual(value1, value2, omitEmpty);
if (!_flag) {
flag = false;
break;
}
} else if (typeofs === typeofs2) {
// string number boolean null undefined
if (value1 !== value2) {
flag = false;
break;
}
} else {
// 类型不同,不相等
flag = false;
break;
}
}
}
return flag;
} else {
return false;
}
} else {
return false;
}
};
/**
* @description 判断两个数组是否相等
* @param {any[]} arr1
* @param {any[]} arr2
* @param {boolean} omitEmpty 默认为false,当为true时,为空的值判断为相等
* @return {boolean}
*/
export const isArrayEqual = function (
arr1: any[],
arr2: any[],
omitEmpty = false
) {
if (arr1.length === arr2.length) {
let flag = true;
let i = 0;
for (const item of arr1) {
const value1 = item;
const value2 = arr2[i];
const typeofs = Object.prototype.toString.call(value1);
const typeofs2 = Object.prototype.toString.call(value2);
// 如果两个都为空,判断为相等
if (omitEmpty && isEmpty(value1) && isEmpty(value2)) {
// flag = true;
} else {
if (typeofs === '[object Object]' && typeofs2 === '[object Object]') {
const _flag = isObjectEqual(value1, value2, omitEmpty);
if (!_flag) {
flag = false;
break;
}
} else if (
typeofs === '[object Array]' &&
typeofs2 === '[object Array]'
) {
const _flag = isArrayEqual(value1, value2, omitEmpty);
if (!_flag) {
flag = false;
break;
}
} else if (typeofs === typeofs2) {
// string number boolean null undefined
if (value1 !== value2) {
flag = false;
break;
}
} else {
// 类型不同,不相等
flag = false;
break;
}
}
i++;
}
return flag;
} else {
return false;
}
};
/**
* @description 判断两个数据是否相等
* @param {any} data1
* @param {any} data2
* @param {boolean} omitEmpty 默认为false,当为true时,为空的值判断为相等
* @return {boolean}
*/
export const isEqual = function (data1: any, data2: any, omitEmpty = false) {
let flag = true;
// 如果两个都为空,判断为相等
if (omitEmpty && isEmpty(data1) && isEmpty(data2)) {
// flag = true;
} else {
const typeofs = Object.prototype.toString.call(data1);
const typeofs2 = Object.prototype.toString.call(data2);
if (typeofs === '[object Object]' && typeofs2 === '[object Object]') {
const _flag = isObjectEqual(data1, data2, omitEmpty);
if (!_flag) {
flag = false;
}
} else if (typeofs === '[object Array]' && typeofs2 === '[object Array]') {
const _flag = isArrayEqual(data1, data2, omitEmpty);
if (!_flag) {
flag = false;
}
} else if (typeofs === typeofs2) {
// string number boolean null undefined
if (data1 !== data2) {
flag = false;
}
} else {
// 类型不同,不相等
flag = false;
}
}
return flag;
};
import { isEmpty } from './isEmpty';
type IndexKey = string | number | symbol;
interface AnyObject {
[propName: IndexKey]: any;
}
/**
* @description 去除对象中为空的属性值 返回一个新对象
* @param {AnyObject} data
* @return 返回一个新对象
*/
export const omitEmpty = function (data: AnyObject) {
const typeofs = Object.prototype.toString.call(data);
if (typeofs === '[object Object]') {
const obj: AnyObject = {};
for (const key in data) {
if (!isEmpty(data[key])) {
obj[key] = data[key];
}
}
return obj;
} else {
console.error(`Expect type [object Object], but get type ${typeofs}`);
return data;
}
};
......@@ -5,3 +5,10 @@
/*************** ag grid vue3****************/
import 'ag-grid-enterprise';
import 'src/css/ag-grid.scss';
/*************** wangEditor 5 css****************/
import 'src/css/wang-editor5.scss';
/*************** CKEditor 5 language ****************/
import '@ckeditor/ckeditor5-build-classic/build/translations/zh-cn.js'; /* 中文简体 */
import '@ckeditor/ckeditor5-build-classic/build/translations/zh.js'; /* 中文繁体 */
......@@ -26,4 +26,58 @@ export default [
link: '/components-example',
active: false,
},
{
title: 'Pinia example',
caption: 'vue状态管理',
icon: 'fa-solid fa-cube',
link: '/pinia-example',
active: false,
},
{
title: 'vue状态管理',
caption: null,
icon: 'fa-solid fa-cube',
link: '/state-mgt',
active: false,
},
{
title: '富文本编辑器',
caption: null,
children: [
{
title: 'wangEditor 5',
caption: null,
icon: 'fa-solid fa-file-pen',
link: '/wang-editor5',
active: false,
},
{
title: 'CKEditor 5 ',
caption: null,
icon: 'fa-solid fa-file-pen',
link: '/ck-editor5',
active: false,
},
],
},
{
title: 'Other',
caption: null,
children: [
{
title: '一些css',
caption: null,
icon: 'fa-brands fa-css3',
link: '/some-css',
active: false,
},
{
title: '数组批量移动',
caption: null,
icon: 'fa-brands fa-css3',
link: '/array-bulk-move',
active: false,
},
],
},
];
......@@ -7,6 +7,9 @@
.container-height {
height: calc(100vh - 50px);
}
.flex-1 {
flex: 1;
}
// 必填
.text-required::before {
......
@import '@wangeditor/editor/dist/css/style.css';
// view css
.editor-content-view {
border: 3px solid #ccc;
border-radius: 5px;
padding: 0 10px;
margin-top: 20px;
overflow-x: auto;
}
.editor-content-view p,
.editor-content-view li {
white-space: pre-wrap; /* 保留空格 */
}
.editor-content-view blockquote {
border-left: 8px solid #d0e5f2;
padding: 10px 10px;
margin: 10px 0;
background-color: #f1f1f1;
}
.editor-content-view code {
font-family: monospace;
background-color: #eee;
padding: 3px;
border-radius: 3px;
}
.editor-content-view pre > code {
display: block;
padding: 10px;
}
.editor-content-view table {
border-collapse: collapse;
}
.editor-content-view td,
.editor-content-view th {
border: 1px solid #ccc;
min-width: 50px;
height: 20px;
}
.editor-content-view th {
background-color: #f1f1f1;
}
.editor-content-view ul,
.editor-content-view ol {
padding-left: 20px;
}
.editor-content-view input[type='checkbox'] {
margin-right: 5px;
}
import { SlateDescendant, SlateElement, SlateText } from '@wangeditor/editor';
declare module '@wangeditor/editor' {
// 扩展 Text
interface SlateText {
text: string;
}
// 扩展 Element
interface SlateElement {
type: string;
children: SlateDescendant[];
}
}
<template>
<div class="list-box">
<q-expansion-item
v-if="children && children.length > 0"
expand-separator
......@@ -9,7 +10,7 @@
<q-item
clickable
class="my-essential--item"
active-class="bg-primary-bg-light"
active-class="my-item-active-class"
v-for="item in children"
@click="expansionClick(item)"
:key="item.link"
......@@ -37,9 +38,13 @@
@click="onClick"
:active="pageStore.activeRouter?.path === link ? true : false"
class="my-essential--item"
active-class="bg-primary-bg-light"
active-class="my-item-active-class"
>
<q-item-section
v-if="icon"
avatar
class="my-essential-link--item-section"
>
<q-item-section v-if="icon" avatar class="my-essential-link--item-section">
<q-icon :name="icon" />
</q-item-section>
......@@ -48,6 +53,7 @@
<q-item-label caption v-if="caption">{{ caption }}</q-item-label>
</q-item-section>
</q-item>
</div>
</template>
<script lang="ts">
......@@ -112,6 +118,13 @@ export default defineComponent({
<style lang="scss" scoped>
.my-essential--item {
align-items: center;
color: #4e342e;
}
.list-box {
:deep(.my-item-active-class) {
color: $primary;
background: $primary-bg-light !important;
}
}
.my-essential-link--item-section {
width: 24px !important;
......
<!--
* @FileDescription: Ag Grid Selection
* @Date: 2023-08-07
* @LastEditTime: 2023-08-08
* @LastEditTime: 2023-10-08
-->
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
......@@ -180,8 +180,5 @@ function removeItem() {
.ag-grid-selection {
display: flex;
flex-direction: column;
.table-box {
//
}
}
</style>
export default [
{
path: 'ag-grid-selection',
name: 'AG_GRID_SELECTION',
name: 'AgGridSelection',
component: () => import('./AgGridSelection.vue'),
meta: {
title: 'Ag Grid Selection',
......
<!--
* @FileDescription: 数组批量上移、下移
* @Date: 2023-10-16
* @LastEditTime: 2023-10-16
-->
<script setup lang="ts">
import { reactive } from 'vue';
import { useMessage } from 'src/common/hooks';
const { warn } = useMessage();
const state = reactive({
list: [
{
title: '1',
checked: false,
},
{
title: '2',
checked: false,
}, {
title: '3',
checked: false,
}, {
title: '4',
checked: false,
}, {
title: '5',
checked: false,
},
{
title: '6',
checked: false,
},
{
title: '7',
checked: false,
},
],
})
function bulkUp() {
const selected = state.list.filter(i => i.checked);
// console.log('selected=', selected);
if (selected.length === 0) return warn('请选择数据');
state.list = bulkUpContinue(selected, state.list, 'title');
// console.log('上移list', state.list);
}
/**
* @description 批量上移
* @param {any[]} selected 勾选的数据列表
* @param {any[]} target 当前数组列表
* @param {string} uniqueKey 唯一的key
* @return {any[]} 返回的新数组列表
*/
function bulkUpContinue(selected: any[], target: any[], uniqueKey: string) {
if (selected.length > 0) {
const item = selected[0];
const afterMoveList = upOne(item, target, uniqueKey);
if (selected.length > 1) {
const afterMoveSelected = selected.slice(1);
return bulkUpContinue(afterMoveSelected, afterMoveList, uniqueKey);
} else {
return afterMoveList;
}
} else {
return target;
}
}
/**
* @description 单个上移
* @param {any} checked 勾选的一条数据
* @param {any[]} target 当前数组列表
* @param {string} uniqueKey 唯一的key
* @return {any[]} 返回的新数组列表
*/
function upOne(checked: any, target: any[], uniqueKey: string) {
if (target.length === 0) {
return [];
} else {
const key = checked[uniqueKey];
// 勾选数据的下标
const index = target.findIndex(i => i[uniqueKey] === key);
// 勾选数据的前一个下标
const lastIndex = index === 0 ? target.length - 1 : index - 1;
const newList = [] as any[];
if (index === 0) {
let i = 0;
for (const item of target) {
if (i > 0) {
newList.push(item);
}
i = i + 1;
};
newList.push(target[0]);
} else {
let i = 0;
for (const item of target) {
if (i === lastIndex) {
newList.push(target[index]);
} else if (i === index) {
newList.push(target[lastIndex]);
} else {
newList.push(item);
}
i = i + 1;
}
}
return newList;
}
}
// function oneUp() {
// const selected = state.list.filter(i => i.checked);
// if (selected.length === 1) {
// const list = upOne(selected[0], state.list);
// state.list = list;
// console.log('list', list);
// } else {
// return;
// }
// }
// function oneDown() {
// const selected = state.list.filter(i => i.checked);
// if (selected.length === 1) {
// const list = downOne(selected[0], state.list, 'title');
// state.list = list;
// console.log('list', list);
// } else {
// return;
// }
// }
function batchDown() {
const selected = state.list.filter(i => i.checked);
// console.log('selected=', selected);
if (selected.length === 0) return warn('请选择数据');
state.list = batchDownContinue(selected, state.list, 'title');
// console.log('下移list', state.list);
}
/**
* @description 批量下移
* @param {any[]} selected 勾选的数据列表
* @param {any[]} target 当前数组列表
* @param {string} uniqueKey 唯一的key
* @return {any[]} 返回的新数组列表
*/
function batchDownContinue(selected: any[], target: any[], uniqueKey: string) {
if (selected.length > 0) {
const item = selected[selected.length - 1]; // 从最后一个开始
const afterMoveList = downOne(item, target, uniqueKey);
if (selected.length > 1) {
const afterMoveSelected = selected.slice(0, selected.length - 1);
return batchDownContinue(afterMoveSelected, afterMoveList, uniqueKey);
} else {
return afterMoveList;
}
} else {
return target;
}
}
/**
* @description 单个下移
* @param {any} checked 勾选的一条数据
* @param {any[]} target 当前数组列表
* @param {string} uniqueKey 唯一的key
* @return {any[]} 返回的新数组列表
*/
function downOne(checked: any, target: any[], uniqueKey: string) {
if (target.length === 0) {
return [];
} else {
const key = checked[uniqueKey];
// 勾选数据的下标
const index = target.findIndex(i => i[uniqueKey] === key);
// 勾选数据的后一个下标
const nextIndex = index === target.length - 1 ? 0 : index + 1;
const newList = [] as any[];
if (index === target.length - 1) {
newList.push(target[target.length - 1]);
let i = 0;
for (const item of target) {
if (i < target.length - 1) {
newList.push(item);
}
i = i + 1;
};
} else {
let i = 0;
for (const item of target) {
if (i === index) {
newList.push(target[nextIndex]);
} else if (i === nextIndex) {
newList.push(target[index]);
} else {
newList.push(item);
}
i = i + 1;
}
}
return newList;
}
}
</script>
<template>
<div>
<div>
<q-btn color="primary" label="批量上移" @click="bulkUp" />
<q-btn color="primary" label="批量下移" @click="batchDown" />
<!-- <q-btn color="primary" label="单个上移" @click="oneUp" /> -->
<!-- <q-btn color="primary" label="单个下移" @click="oneDown" /> -->
</div>
<div class="column">
<q-checkbox v-model="i.checked" :label="i.title" v-for="(i) in state.list" :key="i.title" />
</div>
</div>
</template>
<style lang="scss" scoped></style>
export default [
{
path: 'array-bulk-move',
name: 'ArrayBulkMove',
component: () => import('./ArrayBulkMove.vue'),
meta: {
title: '数组批量移动',
permission: ['*'],
keepalive: true,
},
},
];
<!--
* @FileDescription:CKEditor 5
* @Date: 2023-11-13
* @LastEditTime: 2023-11-13
-->
<script setup lang="ts">
import { reactive, shallowRef } from 'vue';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
// import type { EditorConfig } from '@ckeditor/ckeditor5-core';
// const fs = require('fs');
const editorGf = shallowRef<any>({});
const editorState = reactive({
editor: ClassicEditor,
editorData: '<p>这是一行文字。</p>',
editorConfig: {
// The configuration of the editor.
// https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html
placeholder: '请输入...',
language: 'zh-cn',
toolbar: ['undo', 'redo', '|', 'heading', 'bold', 'italic', 'blockQuote', 'link', 'indent', 'outdent', 'numberedList', 'bulletedList'],
}
})
function onReady(editor: any) {
editorGf.value = editor;
console.log('onReady=', editorGf.value);
const names = editorGf.value?.ui.componentFactory.names();
console.log('工具栏项=', Array.from(names));
}
function filechange(event: any) {
const files = event.target.files;
console.log('files', files);
}
function clikeMe() {
// const dir = '../ck-editor5';
// // list all files in the directory
// fs.readdir(dir, (err: any, files: any) => {
// if (err) {
// throw err;
// }
// // files object contains all files names
// // log them on console
// files.forEach((file: any) => {
// console.log(file);
// });
// })
}
</script>
<template>
<div>
<div>
<p>快速开始:<br> https://ckeditor.com/docs/ckeditor5/latest/installation/integrations/vuejs-v3.html</p>
<p>CKEditor 5 rich text editor component for Vue.js: <br> https://www.npmjs.com/package/@ckeditor/ckeditor5-vue
</p>
</div>
<div class="container q-pa-md">
<div>
<ckeditor :editor="editorState.editor" v-model="editorState.editorData" :config="editorState.editorConfig"
@ready="onReady">
</ckeditor>
</div>
</div>
<div>
<p>其它</p>
<button @click="clikeMe">点我</button>
<input type="file" id="fileInput" webkitdirectory directory multiple @change="filechange">
</div>
</div>
</template>
<style lang="scss" scoped>
.container {
height: 500px;
background-color: #ececec;
}
</style>
// 工具栏选项
// [
// 'selectAll', // 全选
// 'undo', // 撤销
// 'redo', // 重做
// 'bold', // 加粗
// 'italic', // 倾斜
// 'blockQuote', // 引用·
// 'link', // 超链接
// 'ckfinder', // 插入图片或文件
// 'uploadImage', // 插入图像
// 'imageUpload', // 插入图像
// 'heading', // 标题
// 'imageTextAlternative', // 更改图片替换文本
// 'toggleImageCaption', // 打开表标题
// 'imageStyle:inline', // 行内
// 'imageStyle:alignLeft', // 图片左侧对齐
// 'imageStyle:alignRight', // 图片右侧对齐
// 'imageStyle:alignCenter', // 图片居中
// 'imageStyle:alignBlockLeft', // 图片左侧对齐
// 'imageStyle:alignBlockRight', // 图片右侧对齐
// 'imageStyle:block', // 图片居中
// 'imageStyle:side', // 图片侧边显示
// 'imageStyle:wrapText', // 文字环绕:图片左侧对齐
// 'imageStyle:breakText', // 文字断行:图片居中
// 'indent', // 增加缩进
// 'outdent', // 减少缩进
// 'numberedList', // 项目编号列表
// 'bulletedList', // 项目符号列表
// 'mediaEmbed',
// 'insertTable',
// 'tableColumn',
// 'tableRow',
// 'mergeTableCells',
// ];
export default [
{
path: 'ck-editor5',
name: 'CkEditor5',
component: () => import('./CkEditor5.vue'),
meta: {
title: 'CKEditor 5 ',
permission: ['*'],
keepalive: true,
},
},
];
export default [
{
path: 'components-example',
name: 'COMPONENTS_EXAMPLE',
name: 'ComponentsExample',
component: () => import('./ComponentsExample.vue'),
meta: {
title: 'Components example',
......
<!--
* @FileDescription: 主页
* @Date: 2023-08-03
* @LastEditTime: 2023-08-07
* @LastEditTime: 2023-10-08
-->
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
......
export default [
{
path: 'home',
name: 'HOME',
name: 'PageHome',
component: () => import('./PageHome.vue'),
meta: {
title: '主页',
......
<!--
* @FileDescription: 登录页面
* @Date: 2023-08-15
* @LastEditTime: 2023-10-08
-->
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useUserInfoStore, useMessage } from 'src/common/hooks';
import { isEmpty } from 'src/common/utils';
const router = useRouter();
const route = useRoute();
const userInfoStore = useUserInfoStore();
const { error } = useMessage();
const ruleRequired = [(val: any) => !isEmpty(val) || '必填'];
const loading = ref(false);
const isPwd = ref(true);
const state = reactive({
formdata: {
username: null,
password: null,
},
});
function onSubmit() {
loading.value = true;
setTimeout(() => {
loading.value = false;
loginSuccess();
}, 1000);
}
function loginSuccess() {
const result = {
id: 1,
username: 'admin',
is_admin: true,
};
if (state.formdata.username !== 'admin') return error('用户名错误');
if (state.formdata.password !== 'admin') return error('密码错误');
userInfoStore.setUserInfo(result);
const redirect = (route.query.redirect || '/home') as string;
router.push({
path: redirect,
});
}
</script>
<template>
<div style="height: 100vh" class="login-page">
<div class="container column">
<div class="title-box">
<div class="title-icon"></div>
</div>
<q-form @submit="onSubmit">
<q-input
v-model="state.formdata.username"
label="用户名"
rounded
outlined
no-error-icon
label-color="primary"
class="login-input"
:rules="ruleRequired"
>
<template v-slot:append>
<q-icon name="fa-solid fa-user" color="brown-6" />
</template>
</q-input>
<q-input
v-model="state.formdata.password"
label="密码"
rounded
outlined
no-error-icon
label-color="primary"
:type="isPwd ? 'password' : 'text'"
class="q-mt-sm login-input"
:rules="ruleRequired"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'fa-solid fa-eye-slash' : 'fa-solid fa-eye'"
class="cursor-pointer"
color="brown-6"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<div style="margin-top: 24px">
<q-btn
unelevated
label="登录"
type="submit"
color="primary"
class="full-width login-btn"
text-color="brown-6"
:loading="loading"
>
<template v-slot:loading>
<q-spinner-facebook />
</template>
</q-btn>
</div>
</q-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-page {
display: flex;
align-items: center;
justify-content: center;
background: url('./imgs/login-bg.png');
background-size: cover;
background-position: center;
.container {
width: 420px;
background-color: transparent;
border: 2px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 10px;
padding: 30px 34px;
> .title-box {
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
> .title-icon {
height: 84px;
width: 84px;
background: url('./imgs/login-icon.svg');
background-size: cover;
background-position: center;
}
}
}
}
.login-btn {
height: 56px;
border-radius: 56px;
font-size: 18px;
letter-spacing: 2px;
}
</style>
This diff is collapsed.
This diff is collapsed.
import { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'LOGIN',
component: () => import('./LoginPage.vue'),
meta: {
title: 'Login',
permission: ['*'],
keepalive: false,
},
},
];
export default routes;
<!--
* @FileDescription: Pinia example
* @Date: 2023-10-08
* @LastEditTime: 2023-10-08
-->
<script setup lang="ts">
import { useCounterStore } from './stores/counter';
const counter = useCounterStore();
counter.count++;
counter.$patch({
count: counter.count + 1,
})
// // 或使用 action 代替
// counter.increment();
</script>
<template>
<div>
<div class="q-pa-md">
<a href="https://pinia.vuejs.org/zh/introduction.html" target="_blank">Pinia文档</a>
</div>
<div class="q-pa-md">
<button>使用</button>
<button>{{ counter.count }}</button>
<button>{{ counter.doubleCount }}</button>
<button>{{ counter.doublePlusOne }}</button>
</div>
</div>
</template>
<style lang="scss" scoped></style>
export default [
{
path: 'pinia-example',
name: 'PiniaExample',
component: () => import('./PiniaExample.vue'),
meta: {
title: 'Pinia example',
permission: ['*'],
keepalive: true,
},
},
];
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 1 };
},
// 也可以这样定义
// state: () => ({ count: 0 })
getters: {
doubleCount(state) {
return state.count * 2;
},
doublePlusOne(): number {
return this.doubleCount + 1;
},
},
actions: {
increment() {
this.count++;
},
},
});
<!--
* @FileDescription: some css
* @Date: 2023-10-09
* @LastEditTime: 2023-10-09
-->
<script setup lang="ts">
// import { reactive } from 'vue';
</script>
<template>
<div class="q-pa-md">
<div class="q-gutter-sm">
<q-card style="width:400px;height:500px;">
<q-card-section class="fit">
<div class="box1">
<div class="box1-inner">
<div class="box1-container">
<div v-for="i in 30" :key="i">{{ i }}</div>
</div>
<div class="btn-box">
<div class="fit row">
<q-btn class="col-3 btn1" flat color="red-6" label="退出" />
<q-btn class="col-9" flat color="blue" label="提交" />
</div>
</div>
</div>
</div>
</q-card-section>
</q-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.box1 {
width: 100%;
height: 100%;
position: relative;
border: 1px solid $border-color;
overflow: hidden;
.box1-inner {
overflow: auto;
width: 100%;
height: 100%;
padding-bottom: 58px;
}
.btn-box {
position: absolute;
width: 100%;
height: 50px;
bottom: 0;
overflow: hidden;
background-color: #fff;
border-top-right-radius: 12px;
border-top-left-radius: 12px;
/* x 偏移量 | y 偏移量 | 阴影模糊半径 | 阴影扩散半径 | 阴影颜色 */
box-shadow: 0px -3px 11px 0px rgba(0, 0, 0, 0.24);
// box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.24);
.btn1 {
position: relative;
&::after {
position: absolute;
content: '';
height: 32px;
width: 1px;
background-color: $border-color;
right: -1px;
top: 10px;
}
}
}
}
</style>
export default [
{
path: 'some-css',
name: 'SomeCss',
component: () => import('./SomeCss.vue'),
meta: {
title: '一些css',
permission: ['*'],
keepalive: true,
},
},
];
<!--
* @FileDescription: 状态管理
* @Date: 2023-10-08
* @LastEditTime: 2023-10-08
-->
<script setup lang="ts">
import { ref } from 'vue';
import Tab1Page from './tabs/Tab1Page.vue';
import Tab2Page from './tabs/Tab2Page.vue';
import Tab3Page from './tabs/Tab3Page.vue';
const tab = ref('tab1');
</script>
<template>
<div class="fit overflow-hidden column no-wrap">
<div>
<q-tabs v-model="tab" dense class="text-grey" active-color="primary" indicator-color="primary" align="left"
narrow-indicator>
<q-tab name="tab1" label="Tab1" />
<q-tab name="tab2" label="Tab2" />
<q-tab name="tab3" label="Tab3" />
</q-tabs>
<q-separator />
</div>
<div class="flex-1 overflow-auto">
<q-tab-panels v-model="tab" animated keep-alive>
<q-tab-panel name="tab1">
<Tab1Page />
</q-tab-panel>
<q-tab-panel name="tab2">
<Tab2Page />
</q-tab-panel>
<q-tab-panel name="tab3">
<Tab3Page />
</q-tab-panel>
</q-tab-panels>
</div>
</div>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { useStore } from '../store';
import { AgGridVue } from 'ag-grid-vue3';
import type { GridOptions, GridApi } from 'ag-grid-community';
const store = useStore();
const isGridReady = ref(false);
const gridApi = ref<GridApi>();
// Ag Grid
const gridOptions: GridOptions<any> = reactive({
suppressContextMenu: true, // 阻止“右键单击”上下文菜单
suppressCellFocus: true, // 阻止单元格聚焦,这意味着键盘导航将对网格单元格禁用
defaultColDef: {
suppressMenu: true, // 阻止此列标头菜单显示
},
columnDefs: [
{
headerName: '序号',
maxWidth: 80,
valueFormatter: (params: any) => {
return params.node?.rowIndex + 1;
},
},
{ headerName: 'Dessert (100g serving)', field: 'name' },
{ headerName: 'Calories', field: 'calories' },
{ headerName: 'Fat (g)', field: 'fat' },
{ headerName: 'Carbs (g)', field: 'carbs' },
{ headerName: 'Protein (g)', field: 'protein' },
{ headerName: 'Sodium (mg)', field: 'sodium' },
{ headerName: 'Calcium (%)', field: 'calcium' },
{ headerName: 'Iron (%)', field: 'iron' },
],
rowData: [],
onGridReady: (params) => {
isGridReady.value = true;
gridApi.value = params.api;
}
});
watch(
() => store.rows,
() => {
doSetMyRows();
},
{
immediate: true,
deep: true
}
)
function doSetMyRows() {
if (isGridReady.value) {
setMyRows();
} else {
setTimeout(() => {
doSetMyRows();
}, 500);
}
}
function setMyRows() {
console.log('变了');
gridApi.value?.setRowData(store.rows)
}
</script>
<template>
<div>
<div style="height:500px;width:100%">
<ag-grid-vue class="ag-theme-material fit" :grid-options="gridOptions">
</ag-grid-vue>
</div>
</div>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
import { reactive, onMounted } from 'vue';
import { useStore } from '../store';
import { getRandomInt } from 'src/common/utils';
const store = useStore();
const state = reactive({
columns: [
{
name: 'name',
required: true,
label: 'Dessert (100g serving)',
align: 'left',
field: 'name',
},
{ name: 'calories', align: 'center', label: 'Calories', field: 'calories', sortable: true },
{ name: 'fat', label: 'Fat (g)', field: 'fat', sortable: true },
{ name: 'carbs', label: 'Carbs (g)', field: 'carbs' },
{ name: 'protein', label: 'Protein (g)', field: 'protein' },
{ name: 'sodium', label: 'Sodium (mg)', field: 'sodium' },
{ name: 'calcium', label: 'Calcium (%)', field: 'calcium', },
{ name: 'iron', label: 'Iron (%)', field: 'iron', }
] as any[],
})
onMounted(() => {
const rows = [
{
name: 'Frozen Yogurt',
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
sodium: 87,
calcium: '14%',
iron: '1%'
},
{
name: 'Ice cream sandwich',
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
sodium: 129,
calcium: '8%',
iron: '1%'
},
{
name: 'Eclair',
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
sodium: 337,
calcium: '6%',
iron: '7%'
},
{
name: 'Cupcake',
calories: 305,
fat: 3.7,
carbs: 67,
protein: 4.3,
sodium: 413,
calcium: '3%',
iron: '8%'
},
{
name: 'Gingerbread',
calories: 356,
fat: 16.0,
carbs: 49,
protein: 3.9,
sodium: 327,
calcium: '7%',
iron: '16%'
},
{
name: 'Jelly bean',
calories: 375,
fat: 0.0,
carbs: 94,
protein: 0.0,
sodium: 50,
calcium: '0%',
iron: '0%'
},
{
name: 'Lollipop',
calories: 392,
fat: 0.2,
carbs: 98,
protein: 0,
sodium: 38,
calcium: '0%',
iron: '2%'
},
{
name: 'Honeycomb',
calories: 408,
fat: 3.2,
carbs: 87,
protein: 6.5,
sodium: 562,
calcium: '0%',
iron: '45%'
},
{
name: 'Donut',
calories: 452,
fat: 25.0,
carbs: 51,
protein: 4.9,
sodium: 326,
calcium: '2%',
iron: '22%'
},
{
name: 'KitKat',
calories: 518,
fat: 26.0,
carbs: 65,
protein: 7,
sodium: 54,
calcium: '12%',
iron: '6%'
}
];
store.setRows(rows);
})
function addRow() {
const length = store.rows.length;
const item = {
name: `Row ${length + 1}`,
calories: getRandomInt(0, 500),
fat: getRandomInt(0, 500),
carbs: getRandomInt(0, 500),
protein: getRandomInt(0, 500),
sodium: getRandomInt(0, 500),
calcium: `${getRandomInt(0, 100)}%`,
iron: `${getRandomInt(0, 100)}%`
};
store.addOne(item);
}
</script>
<template>
<div>
<div class="q-gutter-sm q-mb-sm">
<q-btn label="添加行" color="primary" @click="addRow" />
</div>
<q-table title="Treats" :rows="store.rows" :columns="state.columns" row-key="name" :pagination="{
rowsPerPage: 0
}" />
</div>
</template>
<style lang="scss" scoped></style>
export default [
{
path: 'state-mgt',
name: 'StateMgt',
component: () => import('./StateMgt.vue'),
meta: {
title: '状态管理',
permission: ['*'],
keepalive: true,
},
},
];
import { defineStore } from 'pinia';
export const useStore = defineStore('state-mgt-store', {
state: () => {
return { rows: [] as any[] };
},
// 也可以这样定义
// state: () => ({ count: 0 })
getters: {},
actions: {
setRows(data: any[]) {
this.rows = data;
},
addOne(data: any) {
this.rows.push(data);
},
},
});
<script setup lang="ts">
// import { reactive } from 'vue';
import TablePage1 from '../elements/TablePage1.vue';
</script>
<template>
<div>
<TablePage1 />
</div>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
// import { reactive } from 'vue';
import AgGridTable from '../elements/AgGridTable.vue';
</script>
<template>
<div>
<AgGridTable />
</div>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
// import { reactive } from 'vue';
</script>
<template>
<div>
<div class="text-h6">Tab3</div>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</div>
</template>
<style lang="scss" scoped></style>
<!--
* @FileDescription: 乱七八糟
-->
<script setup lang="ts">
// import { reactive } from 'vue';
</script>
<template>
<div>
<div>123</div>
</div>
</template>
<style lang="scss" scoped></style>
export default [
{
path: 'test',
name: 'TestPage',
component: () => import('./TestPage.vue'),
meta: {
title: '测试',
permission: ['*'],
keepalive: true,
},
},
];
<!--
* @FileDescription: 富文本编辑器 wangEditor5 https://github.com/wangeditor-team/wangEditor
* @Date: 2023-10-16
* @LastEditTime: 2023-10-16
-->
<script setup lang="ts">
import { onMounted, ref, shallowRef, onBeforeUnmount } from 'vue';
import { createEditor, createToolbar } from '@wangeditor/editor';
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
const viewHtml = ref('');
onMounted(() => {
initEditer();
})
onBeforeUnmount(() => {
// 组件销毁时,也及时销毁编辑器
const editor = editorRef.value
if (editor == null) return;
editor.destroy();
})
function initEditer() {
// editor
const editorConfig = {
placeholder: '请输入内容...',
};
const editor = createEditor({
selector: '#editor-container',
html: '<p><br></p>',
config: editorConfig,
mode: 'simple', // 'default' or 'simple'
});
// 记录 editor 实例
editorRef.value = editor;
//
// toolbar
const toolbarConfig = {};
createToolbar({
editor,
selector: '#toolbar-container',
config: toolbarConfig,
mode: 'simple', // 'default' or 'simple'
})
}
// 获取 HTML 内容
function getHtml() {
const editor = editorRef.value;
const html = editor.getHtml();
viewHtml.value = html;
console.log('html=', html);
}
// 获取纯文本内容
function getText() {
const editor = editorRef.value;
const text = editor.getText();
console.log('text=', text);
}
</script>
<template>
<div>
<div>快速开始:https://www.wangeditor.com/v5/getting-started.html</div>
<div>
<button @click="getHtml">getHtml</button>
<button @click="getText">getText</button>
</div>
<div class="q-pa-md">
<div id="editor-wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
</div>
<div class="q-pa-md">
<div class="view-box editor-content-view">
<div v-html="viewHtml"></div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
#editor-wrapper {
border: 1px solid $border-color;
z-index: 100;
/* 按需定义 */
}
#toolbar-container {
border-bottom: 1px solid $border-color;
display: flex;
flex-direction: row;
}
#editor-container {
height: 500px;
background-color: pink;
}
.view-box {
overflow: auto;
border: 1px solid $border-color;
height: 400px;
}
</style>
export default [
{
path: 'wang-editor5',
name: 'WangEditor5',
component: () => import('./WangEditor5.vue'),
meta: {
title: 'wangEditor 5',
permission: ['*'],
keepalive: true,
},
},
];
......@@ -2,7 +2,7 @@
<q-page class="container-height">
<div class="fit overflow-auto">
<router-view v-slot="{ Component, route }">
<keep-alive :include="keepAliveList">
<keep-alive :include="state.keepAliveList">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
......@@ -15,18 +15,35 @@
// v-if="!(route.meta && route.meta.keepalive)"
// :is="Component"
// />
import { computed } from 'vue';
import { watch, reactive } from 'vue';
import { usePageStore } from 'src/common/hooks';
export default {
name: 'LaoutIndexPage',
setup() {
const pageStore = usePageStore();
const keepAliveList = computed(() => {
return pageStore.allPageKeys;
});
// const keepAliveList = computed(() => {
// console.log('keepAliveList--', pageStore.allPageKeys);
// return pageStore.allPageKeys;
// });
const state = reactive({
keepAliveList: [] as any[]
})
watch(
() => pageStore.allPageKeys,
(val) => {
state.keepAliveList = val;
},
{
immediate: true,
deep: true,
}
)
return {
keepAliveList,
// keepAliveList,
state,
};
},
};
......
......@@ -7,6 +7,7 @@ import {
} from 'vue-router';
import routes from './routes';
import { useUserInfoStore } from 'src/common/hooks';
/*
* If not building with SSR mode, you can
......@@ -20,7 +21,9 @@ import routes from './routes';
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
......@@ -32,5 +35,31 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE),
});
Router.beforeEach((to: any, from: any, next: any) => {
const userInfoStore = useUserInfoStore();
// console.log('to', to);
// console.log(userInfoStore.$state);
if (to.matched.length === 0) {
next({ name: 'NOT_FOUND' });
} else {
if (to.path === '/login') {
next();
} else {
if (userInfoStore.$state.id > 0) {
next();
} else {
next({
path: '/login',
query: {
redirect: to.fullPath, // 把要跳转的页面的路径作为参数传到登录页面
},
});
}
}
}
});
return Router;
});
import { RouteRecordRaw } from 'vue-router';
import LOGIN from '../modules/login/router';
import HOME from '../modules/home/router';
import AG_GRID_SELECTION from '../modules/ag-grid-selection/router';
import COMPONENTS_EXAMPLE from '../modules/components-example/router';
import PINIA_EXAMPLE from '../modules/pinia-example/router';
import STATE_MGT from '../modules/state-mgt/router';
import SOME_CSS from '../modules/some-css/router';
import ARRAY_BULK_MOVE from '../modules/array-bulk-move/router';
import WANG_EDITOR5 from '../modules/wang-editor5/router';
import CK_EDITOR5 from '../modules/ck-editor5/router';
import TEST from '../modules/test/router';
const routes: RouteRecordRaw[] = [
{
......@@ -13,15 +21,27 @@ const routes: RouteRecordRaw[] = [
name: 'LaoutIndexPage',
component: () => import('pages/IndexPage.vue'),
redirect: '/home',
children: [...HOME, ...AG_GRID_SELECTION, ...COMPONENTS_EXAMPLE],
children: [
...HOME,
...AG_GRID_SELECTION,
...COMPONENTS_EXAMPLE,
...PINIA_EXAMPLE,
...STATE_MGT,
...SOME_CSS,
...ARRAY_BULK_MOVE,
...WANG_EDITOR5,
...CK_EDITOR5,
...TEST,
],
},
],
},
...LOGIN,
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
name: 'NOT_FOUND',
component: () => import('pages/ErrorNotFound.vue'),
},
];
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment