Squashed commit of the following:
All checks were successful
local-test分支构建发布 / Docker-Build (push) Successful in 2m36s

commit 6296877798
Author: joylink_zhaoerwei <Bob_Engineer@163.com>
Date:   Fri Sep 6 11:08:37 2024 +0800

    权限代码备用

commit be4df53219
Author: joylink_zhaoerwei <Bob_Engineer@163.com>
Date:   Mon Jul 15 16:10:30 2024 +0800

    同步北京部分计轴

commit 20d39f9f4a
Author: joylink_zhaoerwei <Bob_Engineer@163.com>
Date:   Mon Jul 15 15:41:09 2024 +0800

    同步区段复制控制-A键直接复制到端点

commit d345a1dc87
Author: joylink_zhaoerwei <Bob_Engineer@163.com>
Date:   Mon Jul 15 15:17:30 2024 +0800

    前端框架版本升级+同步北京区段道岔移动时端点吸附
This commit is contained in:
joylink_zhaoerwei 2024-09-06 11:11:04 +08:00
parent 45b34cb3b9
commit 076b366137
15 changed files with 1266 additions and 29 deletions

View File

@ -1,4 +1,4 @@
API=192.168.3.233:9081
API=192.168.33.233:9081
HTTP=http://
NS=
WS=ws://

View File

@ -23,7 +23,7 @@
"centrifuge": "^4.0.1",
"dotenv": "^16.3.1",
"google-protobuf": "^3.21.2",
"jl-graphic": "git+https://gitea.joylink.club/joylink/graphic-pixi.git#v0.1.3",
"jl-graphic": "git+https://gitea.joylink.club/joylink/graphic-pixi.git#v0.1.15",
"js-base64": "^3.7.5",
"pinia": "^2.0.11",
"quasar": "^2.6.0",

150
src/api/AuthApi.ts Normal file
View File

@ -0,0 +1,150 @@
import { api } from 'src/boot/axios';
import { PageDto, PageQueryDto } from './ApiCommon';
const AuthBase = '/api/role';
export interface Role {
id: number;
name: string;
}
export class PagingQueryParams extends PageQueryDto {
name?: string;
}
export interface createRoleParams {
name: string;
resList: number[];
}
export interface editRoleParams extends createRoleParams {
id: number;
}
export interface RoleInfo {
id: number;
name: string;
paths: PathItem[];
}
/**
*
* @param params
* @returns
*/
export async function pageQueryRole(
params: PagingQueryParams
): Promise<PageDto<Role>> {
const response = await api.get(`${AuthBase}/role/page`, {
params: params,
});
return response.data;
}
/**
*
* @param data
* @returns
*/
export function createRole(data: createRoleParams) {
return api.post(`${AuthBase}/role/saveOrUpdate`, data);
}
/**
*
* @param id id
*/
export function deleteRole(id: number) {
return api.delete(`${AuthBase}/role/${id}`);
}
/**
*
* @param id id
* @param data
*/
export function saveRoleData(data: editRoleParams) {
return api.post(`${AuthBase}/role/saveOrUpdate`, data);
}
/**
*
* @param id id
* @returns
*/
export async function getRoleInfo(id: number): Promise<RoleInfo> {
const response = await api.get(`${AuthBase}/role/${id}`);
return response.data;
}
interface LinkRole {
id: number;
roleList: number[];
}
/**
*
* @param params
* @returns
*/
export async function userLinkRole(params: LinkRole): Promise<string> {
const response = await api.post('api/user/edit', params);
return response.data.token;
}
export interface PathItem {
id: number;
method: string;
name: string;
path: string;
}
/**
*
* @param params
* @returns
*/
export async function pageQueryPath(
params: PagingQueryParams
): Promise<PageDto<PathItem>> {
const response = await api.get(`${AuthBase}/res/page`, {
params: params,
});
return response.data;
}
/**
*
* @param data
* @returns
*/
export function createPath(data: Omit<PathItem, 'id'>) {
return api.post(`${AuthBase}/res/saveOrUpdate`, data);
}
/**
*
* @param id id
*/
export function deletePath(id: number) {
return api.delete(`${AuthBase}/res/${id}`);
}
/**
*
* @param id id
* @param data
*/
export function savePathData(data: PathItem) {
return api.post(`${AuthBase}/res/saveOrUpdate`, data);
}
/**
*
* @param id id
* @returns
*/
export async function getPathInfo(id: number): Promise<PathItem> {
const response = await api.get(`${AuthBase}/res/${id}`);
return response.data;
}

View File

@ -10,12 +10,13 @@ interface RegisterInfo {
password: string;
}
interface User {
export interface User {
id: string;
name: string;
mobile: string;
password: string;
registerTime: string;
roleList: { roleId: number; roleName: string }[];
}
const PasswordSult = '4a6d74126bfd06d69406fcccb7e7d5d9'; // 密码加盐

View File

@ -0,0 +1,6 @@
export enum MethodType {
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
GET = 'GET',
}

View File

@ -99,6 +99,16 @@ const list = reactive([
label: '用户管理',
icon: 'manage_accounts',
},
{
path: '/sysManage/role',
label: '权限管理',
icon: 'nature_people',
},
{
path: '/sysManage/authPath',
label: '权限接口管理',
icon: 'menu_open',
},
],
},
]);

View File

@ -14,6 +14,9 @@ import {
IGraphicStorage,
ContextMenu,
MenuItemOptions,
calculateDistanceFromPointToLine,
distance2,
getRectangleCenter,
} from 'jl-graphic';
import { IscsFanData } from './graphics/IscsFanInteraction';
import { LinkData } from './graphics/LinkInteraction';
@ -296,6 +299,99 @@ export function initDrawApp(): IDrawApp {
},
})
);
// KeyA 用于区段复制--控制生成的区段位置
const graphicCopyPlugin = app.app.graphicCopyPlugin;
const copySectionListener = new KeyListener({
value: 'KeyA',
global: true,
onPress: () => {
graphicCopyPlugin.updateMoveLimit('sectionPointLimit');
},
});
graphicCopyPlugin.addGraphicControlers([
{
controlerList: [copySectionListener],
check: () => {
if (
graphicCopyPlugin.copys.length == 1 &&
graphicCopyPlugin.copys[0].type == Section.Type
)
return true;
return false;
},
moveLimitOption: {
moveLimitName: 'sectionPointLimit',
moveLimit: (e) => {
const mousePos = app.toCanvasCoordinates(e.global);
const selectSection = app.selectedGraphics[0] as Section;
let selectSectionLeft = selectSection.localToCanvasPoint(
selectSection.getStartPoint()
);
let selectSectionRight = selectSection.localToCanvasPoint(
selectSection.getEndPoint()
);
[selectSectionLeft, selectSectionRight] =
selectSectionLeft.x < selectSectionRight.x
? [selectSectionLeft, selectSectionRight]
: [selectSectionRight, selectSectionLeft];
//要移动到目标位的区段
const sections = app.queryStore.queryByType<Section>(Section.Type);
const minDistanceSection = sections.reduce((prev, cur) => {
const prevDistance = calculateDistanceFromPointToLine(
prev.localToCanvasPoint(prev.getStartPoint()),
prev.localToCanvasPoint(prev.getEndPoint()),
mousePos
);
const curDistance = calculateDistanceFromPointToLine(
cur.localToCanvasPoint(cur.getStartPoint()),
cur.localToCanvasPoint(cur.getEndPoint()),
mousePos
);
return prevDistance > curDistance ||
(prevDistance == curDistance &&
distance2(
prev.localToCanvasPoint(prev.getStartPoint()),
mousePos
) >
distance2(
cur.localToCanvasPoint(cur.getStartPoint()),
mousePos
))
? cur
: prev;
});
const minDistanceRefSectionsPos =
minDistanceSection.localToCanvasPoint(
getRectangleCenter(
minDistanceSection.lineGraphic.getLocalBounds()
)
);
let minDistanceSectionLeft = minDistanceSection.localToCanvasPoint(
minDistanceSection.getStartPoint()
);
let minDistanceSectionRight = minDistanceSection.localToCanvasPoint(
minDistanceSection.getEndPoint()
);
[minDistanceSectionLeft, minDistanceSectionRight] =
minDistanceSectionLeft.x < minDistanceSectionRight.x
? [minDistanceSectionLeft, minDistanceSectionRight]
: [minDistanceSectionRight, minDistanceSectionLeft];
if (mousePos.x > minDistanceRefSectionsPos.x) {
graphicCopyPlugin.container.position.x =
minDistanceSectionRight.x - selectSectionLeft.x;
graphicCopyPlugin.container.position.y =
minDistanceSectionRight.y - selectSectionLeft.y;
} else {
graphicCopyPlugin.container.position.x =
minDistanceSectionLeft.x - selectSectionRight.x;
graphicCopyPlugin.container.position.y =
minDistanceSectionLeft.y - selectSectionRight.y;
}
},
},
},
]);
return drawApp;
}

View File

@ -14,12 +14,12 @@ import {
IAxleCountingData,
AxleCounting,
AxleCountingTemplate,
AxleCountingConsts,
} from './AxleCounting';
import { Section, SectionPort } from '../section/Section';
import { Turnout, TurnoutPort } from '../turnout/Turnout';
import { IRelatedRefData, createRelatedRefProto } from '../CommonGraphics';
import { Signal } from '../signal/Signal';
import { graphicData } from 'src/protos/stationLayoutGraphics';
export interface IAxleCountingDrawOptions {
newData: () => IAxleCountingData;
@ -112,14 +112,7 @@ export class AxleCountingDraw extends GraphicDrawAssistant<
const refData2 = createRelatedRefProto(graphic.type, graphic.id, port);
const axleCounting = new AxleCounting(direction);
axleCounting.loadData(this.graphicTemplate.datas);
if (graphic.type == 'Turnout') {
axleCounting.position.set(ps.x, ps.y);
} else {
axleCounting.position.set(
ps.x,
ps.y - AxleCountingConsts.offsetSection * direction
);
}
axleCounting.id = GraphicIdGenerator.next();
axleCounting.datas.axleCountingRef = [refData2, refData1];
axleCounting.datas.code = `${graphic.datas.code}-${port}+${refGraphic.datas.code}-${refPort}`;
@ -139,14 +132,7 @@ export class AxleCountingDraw extends GraphicDrawAssistant<
const refData = createRelatedRefProto(graphic.type, graphic.id, port);
const axleCounting = new AxleCounting(direction);
axleCounting.loadData(this.graphicTemplate.datas);
if (graphic.type == 'Turnout') {
axleCounting.position.set(ps.x, ps.y);
} else {
axleCounting.position.set(
ps.x,
ps.y - AxleCountingConsts.offsetSection * direction
);
}
axleCounting.id = GraphicIdGenerator.next();
axleCounting.datas.axleCountingRef = [refData];
axleCounting.datas.code = `${graphic.datas.code}-${port}`;
@ -156,9 +142,28 @@ export class AxleCountingDraw extends GraphicDrawAssistant<
}
oneGenerates(height: Point) {
const map = new Map();
const axleCountings = this.app.queryStore.queryByType<AxleCounting>(
AxleCounting.Type
const needDelete: AxleCounting[] = [];
const axleCountings = this.app.queryStore
.queryByType<AxleCounting>(AxleCounting.Type)
.filter((axleCounting) => {
if (axleCounting.datas.axleCountingRef.length == 1) {
const refInfo = axleCounting.datas.axleCountingRef[0];
if (refInfo.deviceType == graphicData.RelatedRef.DeviceType.Section) {
const refSection = this.app.queryStore.queryById<Section>(
refInfo.id
);
if (
refSection.datas.paRef != undefined &&
refSection.datas.pbRef != undefined
) {
needDelete.push(axleCounting);
return false;
}
}
}
return true;
});
this.app.deleteGraphics(...needDelete);
const axleCountingRefs: IRelatedRefData[] = [];
axleCountings.forEach((axleCounting) => {
axleCountingRefs.push(...axleCounting.datas.axleCountingRef);

View File

@ -49,6 +49,7 @@ import { AxleCounting } from '../axleCounting/AxleCounting';
import { LogicSectionData } from 'src/drawApp/graphics/LogicSectionInteraction';
import { LogicSectionDraw } from '../logicSection/LogicSectionDrawAssistant';
import { LogicSection } from '../logicSection/LogicSection';
import { buildDragMoveAbsorbablePositions } from '../turnout/TurnoutDrawAssistant';
export class SectionDraw extends GraphicDrawAssistant<
SectionTemplate,
@ -443,11 +444,13 @@ export class SectionPointEditPlugin extends GraphicInteractionPlugin<Section> {
g.on('selected', this.onSelected, this);
g.on('unselected', this.onUnselected, this);
g.on('_rightclick', this.onContextMenu, this);
g.on('transformstart', this.onDragMove, this);
}
unbind(g: Section): void {
g.off('selected', this.onSelected, this);
g.off('unselected', this.onUnselected, this);
g.off('_rightclick', this.onContextMenu, this);
g.off('transformstart', this.onDragMove, this);
}
onContextMenu(e: FederatedMouseEvent) {
@ -585,4 +588,10 @@ export class SectionPointEditPlugin extends GraphicInteractionPlugin<Section> {
section.draggable = false;
}
}
onDragMove(e: GraphicTransformEvent) {
const section = e.target as Section;
this.app.setOptions({
absorbablePositions: buildDragMoveAbsorbablePositions(section),
});
}
}

View File

@ -17,6 +17,7 @@ import {
AbsorbableLine,
ContextMenu,
MenuItemOptions,
distance,
} from 'jl-graphic';
import {
ITurnoutData,
@ -178,6 +179,126 @@ function buildAbsorbablePositions(turnout: Turnout): AbsorbablePosition[] {
return aps;
}
type dragType = Turnout | Section;
class DragMoveAbsorbablePoint extends AbsorbablePoint {
moveTarget:
| {
position: IPointData;
portPos: IPointData[];
}
| undefined;
constructor(point: IPointData, absorbRange = 15) {
super(point, absorbRange);
}
tryAbsorb(...dragTargets: dragType[]): void {
const dragTarget = dragTargets[0];
if (dragTarget instanceof Turnout) {
if (this.moveTarget == undefined) {
const {
pointA: [A],
pointB: [B],
pointC: [C],
} = dragTarget.datas;
this.moveTarget = {
position: dragTarget.getGlobalPosition(),
portPos: [
dragTarget.localToCanvasPoint(A),
dragTarget.localToCanvasPoint(B),
dragTarget.localToCanvasPoint(C),
],
};
}
const {
pointA: [A],
pointB: [B],
pointC: [C],
} = dragTarget.datas;
[A, B, C].forEach((p, i) => {
const changePos = dragTarget.localToCanvasPoint(p);
if (
distance(this._point.x, this._point.y, changePos.x, changePos.y) <
this.absorbRange &&
this.moveTarget
) {
dragTarget.updatePositionByCanvasPosition(
new Point(
this.moveTarget.position.x +
this._point.x -
this.moveTarget.portPos[i].x,
this.moveTarget.position.y +
this._point.y -
this.moveTarget.portPos[i].y
)
);
}
});
} else {
if (this.moveTarget == undefined) {
this.moveTarget = {
position: dragTarget.getGlobalPosition(),
portPos: [
dragTarget.localToCanvasPoint(dragTarget.getStartPoint()),
dragTarget.localToCanvasPoint(dragTarget.getEndPoint()),
],
};
}
dragTarget
.localToCanvasPoints(...dragTarget.datas.points)
.forEach((p, i) => {
if (
distance(this._point.x, this._point.y, p.x, p.y) <
this.absorbRange &&
this.moveTarget
) {
dragTarget.updatePositionByCanvasPosition(
new Point(
this.moveTarget.position.x +
this._point.x -
this.moveTarget.portPos[i].x,
this.moveTarget.position.y +
this._point.y -
this.moveTarget.portPos[i].y
)
);
}
});
}
}
}
export function buildDragMoveAbsorbablePositions(
target: dragType
): AbsorbablePosition[] {
const aps: AbsorbablePosition[] = [];
const sections = target.queryStore.queryByType<Section>(Section.Type);
sections.forEach((section) => {
if (section.id !== target.id) {
section.localToCanvasPoints(...section.datas.points).forEach((p) => {
aps.push(new DragMoveAbsorbablePoint(p)); //区段端点
});
}
});
const turnouts = target.queryStore.queryByType<Turnout>(Turnout.Type);
turnouts.forEach((otherTurnout) => {
if (otherTurnout.id !== target.id) {
const {
pointA: [A],
pointB: [B],
pointC: [C],
} = otherTurnout.datas;
[A, B, C].forEach((p) => {
aps.push(
new DragMoveAbsorbablePoint(otherTurnout.localToCanvasPoint(p)) //道岔端点
);
});
}
});
return aps;
}
function onEditPointCreate(turnout: Turnout, dp: DraggablePoint) {
dp.on('transformstart', (e: GraphicTransformEvent) => {
if (e.isShift()) {
@ -290,6 +411,7 @@ export class TurnoutPointsInteractionPlugin extends GraphicInteractionPlugin<Tur
g.transformSave = true;
g.on('selected', this.onSelected, this);
g.on('unselected', this.onUnSelected, this);
g.on('transformstart', this.onDragMove, this);
}
unbind(g: Turnout): void {
@ -298,6 +420,7 @@ export class TurnoutPointsInteractionPlugin extends GraphicInteractionPlugin<Tur
g.graphics.sections.forEach((sectionGraphic) => {
sectionGraphic.off('rightclick');
});
g.off('transformstart', this.onDragMove, this);
}
onSelected(g: DisplayObject) {
@ -327,6 +450,13 @@ export class TurnoutPointsInteractionPlugin extends GraphicInteractionPlugin<Tur
filter(...grahpics: JlGraphic[]): Turnout[] | undefined {
return grahpics.filter((g) => g.type == Turnout.Type) as Turnout[];
}
onDragMove(e: GraphicTransformEvent) {
const turnout = e.target as Turnout;
this.app.setOptions({
absorbablePositions: buildDragMoveAbsorbablePositions(turnout),
});
}
}
type onTurnoutEditPointCreate = (turnout: Turnout, dp: DraggablePoint) => void;

View File

@ -0,0 +1,375 @@
<template>
<div class="q-pa-md">
<q-table
ref="tableRef"
title="权限接口列表"
:style="{ height: tableHeight + 'px' }"
class="my-sticky-virtscroll-table"
:rows="rows"
:columns="columnDefs"
row-key="id"
v-model:pagination="pagination"
:rows-per-page-options="[10, 20, 50, 100]"
:loading="loading"
:filter="filter"
:selection="isRole ? 'multiple' : 'none'"
v-model:selected="selected"
@update:selected="tableSelected"
:selected-rows-label="getSelectedString"
binary-state-sort
@request="onRequest"
>
<template v-slot:top-right>
<q-input
dense
debounce="1000"
v-model="filter.name"
label="名称"
></q-input>
<q-btn flat round color="primary" icon="search" />
<q-btn
v-if="!isRole"
color="primary"
label="新建"
@click="editFormShow = true"
/>
</template>
<template v-slot:header-selection="scope">
<q-checkbox v-model="scope.selected" />
</template>
<template v-slot:body-selection="scope">
<q-checkbox v-model="scope.selected" />
</template>
<template v-slot:body-cell-operations="props">
<q-td :props="props">
<div class="q-gutter-sm row justify-center">
<q-btn
color="primary"
label="编辑"
@click="edieAuthData(props.row)"
/>
<q-btn
color="red"
:disable="operateDisabled"
label="删除"
@click="deleteAuthData(props.row)"
/>
</div>
</q-td>
</template>
</q-table>
<q-dialog
v-model="editFormShow"
persistent
transition-show="scale"
transition-hide="scale"
>
<q-card style="width: 300px">
<q-card-section>
<q-form
ref="myForm"
@submit="edieAuthPath"
@reset="onReset"
class="q-gutter-md"
>
<div class="text-h6">{{ pathInfo.id ? '修改' : '新建' }}</div>
<q-input outlined label="名称" v-model="pathInfo.name" />
<q-input outlined label="接口" v-model="pathInfo.path" />
<q-select
outlined
v-model="pathInfo.methodList"
:options="options"
multiple
map-options
emit-value
label="方法"
lazy-rules
:rules="[(val) => val.length > 0 || '请选择方法!']"
></q-select>
<q-card-actions align="right">
<q-btn color="primary" label="保存" type="submit" />
<q-btn label="取消" type="reset" v-close-popup />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue';
import { useQuasar, type QTableColumn, QForm } from 'quasar';
import {
createPath,
deletePath,
getPathInfo,
pageQueryPath,
PathItem,
savePathData,
} from '../api/AuthApi';
import { successNotify } from '../utils/CommonNotify';
import { ApiError } from 'src/boot/axios';
import { MethodType } from 'src/components/AuthData';
import { useRoute } from 'vue-router';
const $q = useQuasar();
interface SelectItem {
id: number;
}
const props = withDefaults(
defineProps<{
sizeHeight: number;
selects?: PathItem[];
}>(),
{ sizeHeight: 500, selects: () => [] }
);
const tableHeight = computed(() => {
return props.sizeHeight - 32;
});
onMounted(() => {
tableRef.value.requestServerInteraction();
selected.value = props.selects;
});
const columnDefs: QTableColumn[] = [
{ name: 'id', label: 'ID', field: 'id', align: 'center' },
{
name: 'name',
label: '权限接口名称',
field: 'name',
required: true,
align: 'center',
},
{
name: 'path',
label: '接口路径',
field: 'path',
required: true,
align: 'center',
},
{
name: 'method',
label: '方法',
field: (row) => {
return getMethodName(row.method);
},
align: 'center',
},
{ name: 'operations', label: '操作', field: 'operations', align: 'center' },
];
const operateDisabled = ref(false);
const tableRef = ref();
const rows = reactive([]);
const filter = reactive({
name: '',
});
const loading = ref(false);
const pagination = ref({
sortBy: 'desc',
descending: false,
page: 1,
rowsPerPage: 10,
rowsNumber: 10,
});
// eslint-disable-next-line
async function onRequest(props: any) {
const { page, rowsPerPage, sortBy, descending } = props.pagination;
const filter = props.filter;
loading.value = true;
try {
let response = await pageQueryPath({
name: filter.name,
current: page,
size: rowsPerPage,
});
pagination.value.rowsNumber = response.total;
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
rows.splice(0, rows.length, ...(response.records as []));
} catch (err) {
const error = err as ApiError;
$q.notify({
type: 'negative',
message: error.title,
});
} finally {
loading.value = false;
}
}
const editFormShow = ref(false);
interface PathInfo extends Omit<PathItem, 'id' | 'method'> {
id: string;
methodList: MethodType[];
}
const pathInfo = reactive<PathInfo>({
id: '',
methodList: [],
name: '',
path: '',
});
const options = computed(() => {
const list: { label: string; value: string }[] = [];
for (let item in MethodType) {
const obj = {
label: item,
value: MethodType[item as MethodType],
};
list.push(obj);
}
return list;
});
//
function edieAuthData(row: PathItem) {
getPathInfo(row.id).then((res) => {
pathInfo.id = res.id + '';
pathInfo.name = res.name;
pathInfo.path = res.path;
let list: MethodType[] = [];
if (res.method == '*') {
list = options.value.map((item) => item.value as MethodType);
} else {
list = res.method.split(',') as MethodType[];
}
pathInfo.methodList = list;
editFormShow.value = true;
});
}
const myForm = ref<QForm | null>(null);
//
async function edieAuthPath() {
myForm.value?.validate().then(async (res) => {
if (res) {
operateDisabled.value = true;
try {
let method = pathInfo.methodList.join(',');
const everyM = options.value.every((item) => {
return pathInfo.methodList.includes(item.value as MethodType);
});
if (everyM) {
method = '*';
}
const params = {
name: pathInfo.name,
path: pathInfo.path,
method: method,
};
if (pathInfo.id) {
const cloneParams = Object.assign(params, { id: +pathInfo.id });
await savePathData(cloneParams);
} else {
await createPath(params);
}
onReset();
tableRef.value.requestServerInteraction(); //
editFormShow.value = false;
successNotify('保存成功!');
} catch (err) {
const error = err as ApiError;
$q.notify({
type: 'negative',
message: error.title,
});
} finally {
operateDisabled.value = false;
}
}
});
}
function deleteAuthData(row: PathItem) {
operateDisabled.value = true;
$q.dialog({
title: '确认',
message: `确认删除权限接口 "${row.name}" 吗?`,
cancel: true,
})
.onOk(async () => {
try {
await deletePath(row.id);
tableRef.value.requestServerInteraction(); //
} catch (err) {
const error = err as ApiError;
$q.notify({
type: 'negative',
message: error.title,
});
}
})
.onDismiss(() => {
operateDisabled.value = false;
});
}
function onReset() {
pathInfo.id = '';
pathInfo.name = '';
pathInfo.methodList = [];
pathInfo.path = '';
myForm.value?.resetValidation();
}
function getMethodName(val: string) {
let name = val;
if (val == '*') {
const list = options.value.map((item) => item.value);
name = list.join(',');
}
return name;
}
const route = useRoute();
const isRole = computed(() => {
//
return route.path.includes('/sysManage/role');
});
const selected = ref<SelectItem[]>([]);
function getSelectedString() {
const nameArr = selected.value.map((item) => {
return item.id;
});
const name = nameArr.join('');
return `已选ID${name}`;
}
if (isRole.value) {
const index = columnDefs.findIndex((item) => {
return item.name == 'operations';
});
if (index >= 0) {
columnDefs.splice(index, 1);
}
}
const emit = defineEmits(['selectsed']);
function tableSelected() {
emit('selectsed', selected.value);
}
watch(
() => props.selects,
(val) => {
selected.value = val.map((item) => {
return { id: item.id };
});
}
);
</script>

276
src/pages/RoleManage.vue Normal file
View File

@ -0,0 +1,276 @@
<template>
<div class="q-pa-md">
<q-table
ref="tableRef"
title="角色列表"
:style="{ height: tableHeight + 'px' }"
class="my-sticky-virtscroll-table"
:rows="rows"
:columns="columnDefs"
row-key="id"
v-model:pagination="pagination"
:rows-per-page-options="[10, 20, 50, 100]"
:loading="loading"
:filter="filter"
binary-state-sort
@request="onRequest"
>
<template v-slot:top-right>
<q-input
dense
debounce="1000"
v-model="filter.name"
label="名称"
></q-input>
<q-btn flat round color="primary" icon="search" />
<q-btn color="primary" label="新建" @click="editFormShow = true" />
</template>
<template v-slot:body-cell-operations="props">
<q-td :props="props">
<div class="q-gutter-sm row justify-center">
<q-btn
color="primary"
label="编辑"
@click="edieRoleData(props.row)"
:disable="props.row.id == 1"
/>
<q-btn
color="red"
:disable="operateDisabled || [1, 2].includes(props.row.id)"
label="删除"
@click="deleteRoleData(props.row)"
/>
</div>
</q-td>
</template>
</q-table>
<q-dialog
v-model="editFormShow"
persistent
transition-show="scale"
transition-hide="scale"
>
<q-card style="width: 1400px; max-width: 80vw">
<q-form
ref="myForm"
@submit="edieRolePath"
@reset="onReset"
class="q-gutter-md"
>
<q-card-section>
<div class="text-h5">{{ roleInfo.id ? '编辑' : '新建' }}</div>
<div class="q-pa-md">
<q-input
outlined
dense
label="角色名称"
v-model="roleInfo.name"
lazy-rules
:rules="[(val) => val.length > 0 || '请输入角色名称!']"
style="width: 300px"
/>
</div>
<AuthPathManage
:sizeHeight="600"
:selects="roleInfo.paths || []"
@selectsed="pathSelectsed"
/>
<q-card-actions align="right">
<q-btn color="primary" label="保存" type="submit" />
<q-btn label="取消" type="reset" v-close-popup />
</q-card-actions>
</q-card-section>
</q-form>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { useQuasar, type QTableColumn, QForm } from 'quasar';
import {
PathItem,
RoleInfo,
createRole,
createRoleParams,
deleteRole,
getRoleInfo,
pageQueryRole,
saveRoleData,
} from 'src/api/AuthApi';
import { ApiError } from 'src/boot/axios';
import { successNotify } from 'src/utils/CommonNotify';
import AuthPathManage from './AuthPathManage.vue';
const $q = useQuasar();
const props = withDefaults(
defineProps<{
sizeHeight: number;
}>(),
{ sizeHeight: 500 }
);
const tableHeight = computed(() => {
return props.sizeHeight - 32;
});
onMounted(() => {
tableRef.value.requestServerInteraction();
});
const columnDefs: QTableColumn[] = [
{ name: 'id', label: '角色ID', field: 'id', align: 'center' },
{
name: 'name',
label: '角色名称',
field: 'name',
required: true,
align: 'center',
},
{ name: 'operations', label: '操作', field: 'operations', align: 'center' },
];
const operateDisabled = ref(false);
const tableRef = ref();
const rows = reactive([]);
const filter = reactive({
name: '',
});
const loading = ref(false);
const pagination = ref({
sortBy: 'desc',
descending: false,
page: 1,
rowsPerPage: 10,
rowsNumber: 10,
});
// eslint-disable-next-line
async function onRequest(props: any) {
const { page, rowsPerPage, sortBy, descending } = props.pagination;
const filter = props.filter;
loading.value = true;
try {
let response = await pageQueryRole({
name: filter.name,
current: page,
size: rowsPerPage,
});
pagination.value.rowsNumber = response.total;
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
rows.splice(0, rows.length, ...(response.records as []));
} catch (err) {
const error = err as ApiError;
$q.notify({
type: 'negative',
message: error.title,
});
} finally {
loading.value = false;
}
}
const editFormShow = ref(false);
interface RoleItemInfo extends Omit<RoleInfo, 'id'> {
id: string;
editPaths: number[];
}
const roleInfo = reactive<RoleItemInfo>({
id: '',
editPaths: [],
name: '',
paths: [],
});
//
function edieRoleData(row: RoleInfo) {
getRoleInfo(row.id).then((res) => {
roleInfo.id = res.id + '';
roleInfo.name = res.name;
const list = res.paths || [];
roleInfo.paths = list;
roleInfo.editPaths = list.map((item) => item.id);
editFormShow.value = true;
});
}
const myForm = ref<QForm | null>(null);
//
async function edieRolePath() {
myForm.value?.validate().then(async (res) => {
if (res) {
operateDisabled.value = true;
try {
const params: createRoleParams = {
name: roleInfo.name,
resList: roleInfo.editPaths,
};
if (roleInfo.id) {
const cloneParams = Object.assign(params, { id: +roleInfo.id });
await saveRoleData(cloneParams);
} else {
await createRole(params);
}
onReset();
tableRef.value.requestServerInteraction(); //
editFormShow.value = false;
successNotify('保存成功!');
} catch (err) {
const error = err as ApiError;
$q.notify({
type: 'negative',
message: error.title,
});
} finally {
operateDisabled.value = false;
}
}
});
}
function deleteRoleData(row: RoleInfo) {
operateDisabled.value = true;
$q.dialog({
title: '确认',
message: `确认删除角色 "${row.name}" 吗?`,
cancel: true,
})
.onOk(async () => {
try {
await deleteRole(row.id);
tableRef.value.requestServerInteraction(); //
} catch (err) {
const error = err as ApiError;
$q.notify({
type: 'negative',
message: error.title,
});
}
})
.onDismiss(() => {
operateDisabled.value = false;
});
}
function onReset() {
roleInfo.id = '';
roleInfo.name = '';
roleInfo.editPaths = [];
roleInfo.paths = [];
myForm.value?.resetValidation();
}
function pathSelectsed(val: PathItem[]) {
roleInfo.editPaths = val.map((item) => item.id);
}
</script>

View File

@ -24,16 +24,83 @@
></q-input>
<q-btn flat round color="primary" icon="search" />
</template>
<template v-slot:body-cell-roles="props">
<q-td :props="props">
<div class="q-gutter-sm row justify-center">
<q-chip
outline
size="sm"
color="primary"
v-for="(item, index) in props.row.roleList"
:key="index"
>
{{ item.roleName }}
</q-chip>
</div>
</q-td>
</template>
<template v-slot:body-cell-operations="props">
<q-td :props="props">
<div class="q-gutter-sm row justify-center">
<q-btn
color="primary"
label="编辑角色"
:disable="operateDisabled"
@click="edieUserData(props.row)"
/>
</div>
</q-td>
</template>
</q-table>
<q-dialog
v-model="editFormShow"
persistent
transition-show="scale"
transition-hide="scale"
>
<q-card style="width: 300px">
<q-card-section>
<q-form
ref="myForm"
@submit="edieUserRole"
@reset="onReset"
class="q-gutter-md"
>
<div class="text-h6">修改用户角色</div>
<q-input outlined disable label="用户名" v-model="userInfo.name" />
<q-select
outlined
v-model="userInfo.Rids"
:options="options"
multiple
map-options
emit-value
label="用户角色"
lazy-rules
:rules="[(val) => val.length > 0 || '请选择角色!']"
></q-select>
<q-card-actions align="right">
<q-btn color="primary" label="保存" type="submit" />
<q-btn label="取消" type="reset" v-close-popup />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { type QTableColumn } from 'quasar';
import { pageQuery } from '../api/UserApi';
import { errorNotify } from '../utils/CommonNotify';
import { QForm, useQuasar, type QTableColumn } from 'quasar';
import { pageQuery, User } from '../api/UserApi';
import { errorNotify, successNotify } from '../utils/CommonNotify';
import { ApiError } from 'src/boot/axios';
import { pageQueryRole, userLinkRole } from 'src/api/AuthApi';
const $q = useQuasar();
const props = withDefaults(
defineProps<{
sizeHeight: number;
@ -46,6 +113,7 @@ const tableHeight = computed(() => {
onMounted(() => {
tableRef.value.requestServerInteraction();
getAllRole();
});
const columnDefs: QTableColumn[] = [
@ -57,6 +125,13 @@ const columnDefs: QTableColumn[] = [
align: 'center',
},
{ name: 'id', label: '用户ID', field: 'id', align: 'center' },
{
name: 'roles',
label: '角色',
field: 'roles',
required: true,
align: 'center',
},
{
name: 'registerTime',
label: '创建时间',
@ -69,6 +144,7 @@ const columnDefs: QTableColumn[] = [
field: 'mobile',
align: 'center',
},
{ name: 'operations', label: '操作', field: 'operations', align: 'center' },
];
const tableRef = ref();
@ -85,6 +161,7 @@ const pagination = ref({
rowsNumber: 10,
});
// eslint-disable-next-line
async function onRequest(props: any) {
const { page, rowsPerPage, sortBy, descending } = props.pagination;
const filter = props.filter;
@ -109,4 +186,90 @@ async function onRequest(props: any) {
loading.value = false;
}
}
const editFormShow = ref(false);
interface UserInfo extends Omit<User, 'password'> {
Rids: number[];
}
const userInfo = reactive<UserInfo>({
id: '',
name: '',
mobile: '',
registerTime: '',
roleList: [],
Rids: [],
});
const options: { label: string; value: number }[] = [];
function getAllRole() {
pageQueryRole({
current: 1,
size: 999,
})
.then((res) => {
res.records.forEach((item) => {
const obj = {
label: item.name,
value: item.id,
};
options.push(obj);
});
})
.catch((err) => {
console.log(err);
});
}
//
function edieUserData(row: User) {
userInfo.id = row.id;
userInfo.name = row.name;
userInfo.mobile = row.mobile;
userInfo.registerTime = row.registerTime;
userInfo.roleList = row.roleList;
userInfo.Rids = row.roleList.map((item) => {
return item.roleId;
});
editFormShow.value = true;
}
const myForm = ref<QForm | null>(null);
//
const operateDisabled = ref(false);
function edieUserRole() {
myForm.value?.validate().then(async (res) => {
if (res) {
operateDisabled.value = true;
try {
await userLinkRole({
id: +userInfo.id,
roleList: userInfo.Rids,
});
tableRef.value.requestServerInteraction(); //
editFormShow.value = false;
successNotify('修改成功');
} catch (err) {
const error = err as ApiError;
$q.notify({
type: 'negative',
message: error.title,
});
} finally {
operateDisabled.value = false;
}
}
});
}
function onReset() {
userInfo.id = '';
userInfo.name = '';
userInfo.mobile = '';
userInfo.registerTime = '';
userInfo.roleList = [];
userInfo.Rids = [];
myForm.value?.resetValidation();
}
</script>

View File

@ -70,6 +70,22 @@ const routes: RouteRecordRaw[] = [
},
component: () => import('pages/UserManage.vue'),
},
{
path: 'role',
name: 'role',
meta: {
description: '权限管理',
},
component: () => import('pages/RoleManage.vue'),
},
{
path: 'authPath',
name: 'authPath',
meta: {
description: '权限接口管理',
},
component: () => import('pages/AuthPathManage.vue'),
},
],
},
{

View File

@ -2438,9 +2438,9 @@ isobject@^3.0.1:
resolved "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
"jl-graphic@git+https://gitea.joylink.club/joylink/graphic-pixi.git#v0.1.3":
version "0.1.3"
resolved "git+https://gitea.joylink.club/joylink/graphic-pixi.git#100ddafc75ffa2fc646ad26359682e0f083511e3"
"jl-graphic@git+https://gitea.joylink.club/joylink/graphic-pixi.git#v0.1.15":
version "0.1.14"
resolved "git+https://gitea.joylink.club/joylink/graphic-pixi.git#8b0ad14f7324a5eaba58239645a1fa0452e87ab4"
dependencies:
"@pixi/graphics-extras" "^7.3.2"
"@pixi/utils" "^7.3.2"