权限管理、apk包下载
Some checks are pending
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Lint (ubuntu-latest) (push) Waiting to run
CI / Lint (windows-latest) (push) Waiting to run
CI / Check (ubuntu-latest) (push) Waiting to run
CI / Check (windows-latest) (push) Waiting to run
CI / CI OK (push) Blocked by required conditions
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
Deploy Website on push / Deploy Push Playground Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Docs Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Antd Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Element Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Naive Ftp (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
Some checks are pending
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Lint (ubuntu-latest) (push) Waiting to run
CI / Lint (windows-latest) (push) Waiting to run
CI / Check (ubuntu-latest) (push) Waiting to run
CI / Check (windows-latest) (push) Waiting to run
CI / CI OK (push) Blocked by required conditions
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
Deploy Website on push / Deploy Push Playground Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Docs Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Antd Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Element Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Naive Ftp (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
This commit is contained in:
parent
39195fb2e6
commit
5cc74ac42c
@ -40,6 +40,14 @@ export async function addAdminApi(param: UserInfo) {
|
||||
return requestClient.post<UserInfo>('/admin/add', param);
|
||||
}
|
||||
|
||||
export async function editAdminApi(param: UserInfo) {
|
||||
return requestClient.post('/admin/edit', param);
|
||||
}
|
||||
|
||||
export async function deleteAdminApi(id: number) {
|
||||
return requestClient.post('/admin/delete', { id });
|
||||
}
|
||||
|
||||
export async function getAdminConfigList(param:AdminConfigListParam) {
|
||||
return requestClient.post<AdminConfig>('/admin/config/list', param);
|
||||
}
|
||||
|
||||
19
apps/web-antd/src/api/core/apk.ts
Normal file
19
apps/web-antd/src/api/core/apk.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace ApkApi {
|
||||
export type Environment = 'dev' | 'prod' | 'stable';
|
||||
|
||||
export interface PackageItem {
|
||||
downloadPath: string;
|
||||
env: Environment;
|
||||
exists: boolean;
|
||||
fileName: string;
|
||||
size: number;
|
||||
uploadedAt: string;
|
||||
version: string;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApkPackagesApi() {
|
||||
return requestClient.get<ApkApi.PackageItem[]>('/apk/packages');
|
||||
}
|
||||
@ -209,3 +209,7 @@ export async function setUserPermissionsDirectApi(params: PermissionApi.UserPerm
|
||||
export async function getUserRolesApi(admin_id: number) {
|
||||
return requestClient.post<PermissionApi.Role[]>('/admin/user/role/list', { admin_id });
|
||||
}
|
||||
|
||||
export async function getUserRolesBatchApi(admin_ids: number[]) {
|
||||
return requestClient.post<Record<string, PermissionApi.Role[]>>('/admin/user/role/batch-list', { admin_ids });
|
||||
}
|
||||
|
||||
@ -41,7 +41,10 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"analytics": "Analytics",
|
||||
"analytics": "Analytics"
|
||||
},
|
||||
"devops": {
|
||||
"title": "DevOps",
|
||||
"server-list": "Server List",
|
||||
"node-list": "Node List",
|
||||
"mysql-list": "MySQL List",
|
||||
@ -65,6 +68,7 @@
|
||||
},
|
||||
"operation": {
|
||||
"title": "Operation",
|
||||
"apk": "Client APK",
|
||||
"level": "Level",
|
||||
"mail": "Mail",
|
||||
"order": "Order",
|
||||
|
||||
@ -40,8 +40,11 @@
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "工作台",
|
||||
"analytics": "分析台"
|
||||
},
|
||||
"devops": {
|
||||
"title": "运维管理",
|
||||
"analytics": "分析台",
|
||||
"server-list": "区服列表",
|
||||
"node-list": "节点列表",
|
||||
"mysql-list": "MySQL列表",
|
||||
@ -65,6 +68,7 @@
|
||||
},
|
||||
"operation": {
|
||||
"title": "运营管理",
|
||||
"apk": "客户端 APK 下载",
|
||||
"scripts": "自动化脚本",
|
||||
"mail": "邮件管理",
|
||||
"order": "订单管理",
|
||||
|
||||
@ -2,12 +2,18 @@ export interface UserInfo {
|
||||
id?: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
phone: string;
|
||||
real_name?: string;
|
||||
nickname?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
uid?: string;
|
||||
group: string;
|
||||
group?: string;
|
||||
role: number;
|
||||
status?: number;
|
||||
lastLoginTime?: number;
|
||||
lastLoginIp?: string;
|
||||
createTime?: number;
|
||||
updateTime?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -50,13 +50,24 @@ function setupAccessGuard(router: Router) {
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const getResolvedHomePath = async () => {
|
||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||
return userInfo.homePath || DEFAULT_HOME_PATH;
|
||||
};
|
||||
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
const homePath = await getResolvedHomePath();
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
(to.query?.redirect as string) || homePath,
|
||||
);
|
||||
}
|
||||
|
||||
if (to.path === '/' && accessStore.accessToken) {
|
||||
return await getResolvedHomePath();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -89,10 +100,13 @@ function setupAccessGuard(router: Router) {
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||
const userRoles = userInfo.roles ?? [];
|
||||
// 将单点权限码合并进 roles,供路由 authority 字段做精细匹配
|
||||
const userPermissions: string[] = (userInfo as any).permissions ?? [];
|
||||
const accessIdentifiers = [...new Set([...userRoles, ...userPermissions])];
|
||||
|
||||
// 生成菜单和路由
|
||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||
roles: userRoles,
|
||||
roles: accessIdentifiers,
|
||||
router,
|
||||
// 则会在菜单中显示,但是访问会被重定向到403
|
||||
routes: accessRoutes,
|
||||
|
||||
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
||||
icon: 'material-symbols:brightness-empty-outline',
|
||||
order: -1,
|
||||
title: $t('page.admin.title'),
|
||||
authority:['super'],
|
||||
authority: ['super', 'AC1001', 'AC1101', 'AC1107', 'AC1113', 'AC1117'],
|
||||
},
|
||||
name: 'Admin',
|
||||
path: '/admin',
|
||||
@ -20,7 +20,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/user-management',
|
||||
component: () => import('#/views/admin/user/index.vue'),
|
||||
meta: {
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC1001', 'AC1002'],
|
||||
affixTab: false,
|
||||
icon: 'majesticons:user-box-line',
|
||||
title: $t('page.admin.user'),
|
||||
@ -31,7 +31,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/user-management-log',
|
||||
component: () => import('#/views/admin/log/index.vue'),
|
||||
meta: {
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC1003'],
|
||||
affixTab: false,
|
||||
icon: 'material-symbols:assignment-rounded',
|
||||
title: $t('page.admin.log'),
|
||||
@ -42,7 +42,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/user-management-config',
|
||||
component: () => import('#/views/admin/config/index.vue'),
|
||||
meta: {
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC1004', 'AC1005', 'AC1006'],
|
||||
affixTab: false,
|
||||
icon: 'material-symbols:assignment-rounded',
|
||||
title: $t('page.admin.config'),
|
||||
@ -53,7 +53,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/permission/user-group',
|
||||
component: () => import('#/views/admin/permission/user-group.vue'),
|
||||
meta: {
|
||||
authority: ['super'],
|
||||
authority: ['super', 'AC1101', 'AC1102', 'AC1103', 'AC1104', 'AC1105', 'AC1106'],
|
||||
affixTab: false,
|
||||
icon: 'material-symbols:group',
|
||||
title: $t('page.admin.permission.userGroup'),
|
||||
@ -64,7 +64,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/permission/role',
|
||||
component: () => import('#/views/admin/permission/role.vue'),
|
||||
meta: {
|
||||
authority: ['super'],
|
||||
authority: ['super', 'AC1107', 'AC1108', 'AC1109', 'AC1110', 'AC1111', 'AC1112'],
|
||||
affixTab: false,
|
||||
icon: 'material-symbols:shield-person',
|
||||
title: $t('page.admin.permission.role'),
|
||||
@ -75,7 +75,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/permission/permission',
|
||||
component: () => import('#/views/admin/permission/permission.vue'),
|
||||
meta: {
|
||||
authority: ['super'],
|
||||
authority: ['super', 'AC1113', 'AC1114', 'AC1115', 'AC1116'],
|
||||
affixTab: false,
|
||||
icon: 'material-symbols:key',
|
||||
title: $t('page.admin.permission.permission'),
|
||||
@ -86,7 +86,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/permission/user-assign',
|
||||
component: () => import('#/views/admin/permission/user-assign.vue'),
|
||||
meta: {
|
||||
authority: ['super'],
|
||||
authority: ['super', 'AC1117', 'AC1118', 'AC1119', 'AC1120', 'AC1121'],
|
||||
affixTab: false,
|
||||
icon: 'material-symbols:manage-accounts',
|
||||
title: $t('page.admin.permission.userAssign'),
|
||||
|
||||
@ -7,6 +7,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
authority: ['super', 'AC5003', 'AC5004'],
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
@ -19,56 +20,14 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/analytics',
|
||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
affixTab: true,
|
||||
authority: ['super', 'AC5003', 'AC5004'],
|
||||
icon: 'lucide:area-chart',
|
||||
title: $t('page.dashboard.analytics'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ServerList',
|
||||
path: '/server-list',
|
||||
component: () => import('#/views/dashboard/serverList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:gamepad',
|
||||
title: $t('page.dashboard.server-list'),
|
||||
authority: ['super', 'admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AppList',
|
||||
path: '/app-list',
|
||||
component: () => import('#/views/dashboard/appList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:app-window',
|
||||
title: $t('page.dashboard.app-list'),
|
||||
authority: ['super'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NodeList',
|
||||
path: '/node-list',
|
||||
component: () => import('#/views/dashboard/nodeList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:server',
|
||||
title: $t('page.dashboard.node-list'),
|
||||
authority: ['super'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MysqlList',
|
||||
path: '/mysql-list',
|
||||
component: () => import('#/views/dashboard/mysqlList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:database',
|
||||
title: $t('page.dashboard.mysql-list'),
|
||||
authority: ['super'],
|
||||
},
|
||||
},
|
||||
],
|
||||
redirect: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
66
apps/web-antd/src/router/routes/modules/devops.ts
Normal file
66
apps/web-antd/src/router/routes/modules/devops.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'lucide:monitor-cog',
|
||||
order: 0,
|
||||
title: $t('page.devops.title'),
|
||||
authority: ['super', 'AC4004', 'AC4006', 'AC4007', 'AC4008', 'AC4009', 'AC4010', 'AC4011'],
|
||||
},
|
||||
name: 'Devops',
|
||||
path: '/devops',
|
||||
children: [
|
||||
{
|
||||
name: 'ServerList',
|
||||
path: '/server-list',
|
||||
component: () => import('#/views/dashboard/serverList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:gamepad',
|
||||
title: $t('page.devops.server-list'),
|
||||
authority: ['super', 'AC4006', 'AC4010', 'AC4011'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AppList',
|
||||
path: '/app-list',
|
||||
component: () => import('#/views/dashboard/appList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:app-window',
|
||||
title: $t('page.devops.app-list'),
|
||||
authority: ['super', 'AC4007', 'AC4008', 'AC4009'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NodeList',
|
||||
path: '/node-list',
|
||||
component: () => import('#/views/dashboard/nodeList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:server',
|
||||
title: $t('page.devops.node-list'),
|
||||
authority: ['super', 'AC4004', 'AC4005'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MysqlList',
|
||||
path: '/mysql-list',
|
||||
component: () => import('#/views/dashboard/mysqlList/index.vue'),
|
||||
meta: {
|
||||
affixTab: false,
|
||||
icon: 'lucide:database',
|
||||
title: $t('page.devops.mysql-list'),
|
||||
authority: ['super'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.experiment.title'),
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC9201', 'AC9202', 'AC9214'],
|
||||
},
|
||||
name: 'Experiment',
|
||||
path: '/experiment',
|
||||
@ -23,6 +23,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: false,
|
||||
icon: 'lucide:flask-conical',
|
||||
title: $t('page.experiment.abtest'),
|
||||
authority: ['super', 'AC9201', 'AC9202', 'AC9203', 'AC9204', 'AC9205', 'AC9206', 'AC9207', 'AC9208', 'AC9209', 'AC9210', 'AC9211', 'AC9212', 'AC9213'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -33,6 +34,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: false,
|
||||
icon: 'lucide:users',
|
||||
title: $t('page.experiment.groupQuery'),
|
||||
authority: ['super', 'AC9214'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -10,6 +10,7 @@ const routes: RouteRecordRaw[] = [
|
||||
icon: 'solar:book-2-bold',
|
||||
order: 1001,
|
||||
title: $t('page.language.title'),
|
||||
authority: ['super', 'AC9001', 'AC9002', 'AC9003', 'AC9004', 'AC9005'],
|
||||
},
|
||||
name: 'Translate',
|
||||
path: '/translate',
|
||||
@ -22,6 +23,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lets-icons:order',
|
||||
title: $t('page.language.translationList'),
|
||||
authority: ['super', 'AC9001', 'AC9002', 'AC9003', 'AC9004', 'AC9005'],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
||||
icon: 'lucide:bell-ring',
|
||||
order: 1002,
|
||||
title: $t('page.notification.title'),
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC9101', 'AC9102'],
|
||||
},
|
||||
name: 'Notification',
|
||||
path: '/notification',
|
||||
@ -23,7 +23,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lucide:bell-plus',
|
||||
title: $t('page.notification.config'),
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC9101', 'AC9102'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -10,11 +10,22 @@ const routes: RouteRecordRaw[] = [
|
||||
icon: 'lucide:file-clock',
|
||||
order: 1001,
|
||||
title: $t('page.operation.title'),
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC6001', 'AC7001', 'AC8001', 'AC9301', 'AC5002','AC9401'],
|
||||
},
|
||||
name: 'Operation',
|
||||
path: '/operation',
|
||||
children: [
|
||||
{
|
||||
name: 'ApkDownload',
|
||||
path: '/apk-download',
|
||||
component: () => import('#/views/operation/apk/index.vue'),
|
||||
meta: {
|
||||
affixTab: true,
|
||||
icon: 'material-symbols:cloud-download',
|
||||
title: $t('page.operation.apk'),
|
||||
authority: ['super', 'AC9401'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Scripts',
|
||||
path: '/scripts',
|
||||
@ -23,7 +34,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lucide:chart-no-axes-column-increasing',
|
||||
title: $t('page.operation.scripts'),
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC9301', 'AC9302', 'AC9303'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -34,6 +45,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lucide:mail',
|
||||
title: $t('page.operation.mail'),
|
||||
authority: ['super', 'AC7001', 'AC7002', 'AC7003'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -44,6 +56,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lucide:mail',
|
||||
title: $t('page.operation.copyUser'),
|
||||
authority: ['super', 'AC8001'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -54,6 +67,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lets-icons:order',
|
||||
title: $t('page.operation.order'),
|
||||
authority: ['super', 'AC5002'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -64,6 +78,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lets-icons:order',
|
||||
title: $t('page.operation.activity'),
|
||||
authority: ['super', 'AC6001', 'AC6002', 'AC6003', 'AC6004', 'AC6005'],
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
||||
icon: 'lucide:laugh',
|
||||
order: 1000,
|
||||
title: $t('page.userlog.title'),
|
||||
authority: ['super', 'admin'],
|
||||
authority: ['super', 'AC2001', 'AC2002', 'AC2003', 'AC2004', 'AC2005', 'AC3001', 'AC3003', 'AC3004'],
|
||||
},
|
||||
name: 'Userlog',
|
||||
path: '/userlog',
|
||||
@ -23,6 +23,7 @@ const routes: RouteRecordRaw[] = [
|
||||
affixTab: true,
|
||||
icon: 'lucide:list',
|
||||
title: $t('page.userlog.userlist'),
|
||||
authority: ['super', 'AC3001', 'AC3002', 'AC3003', 'AC3004'],
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
@ -46,7 +46,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
userInfo = fetchUserInfoResult;
|
||||
userStore.setUserInfo(userInfo);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
const mergedAccessCodes = [
|
||||
...new Set([...(accessCodes || []), ...(((userInfo as any)?.permissions || []) as string[])]),
|
||||
];
|
||||
accessStore.setAccessCodes(mergedAccessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
@ -96,7 +99,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
userInfo = fetchUserInfoResult;
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
const mergedAccessCodes = [
|
||||
...new Set([...(accessCodes || []), ...(((userInfo as any)?.permissions || []) as string[])]),
|
||||
];
|
||||
accessStore.setAccessCodes(mergedAccessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
@ -147,6 +153,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
let userInfo: null | UserInfo = null;
|
||||
userInfo = await getUserInfoApi();
|
||||
userStore.setUserInfo(userInfo);
|
||||
const mergedAccessCodes = [
|
||||
...new Set([
|
||||
...(accessStore.accessCodes || []),
|
||||
...(((userInfo as any)?.permissions || []) as string[]),
|
||||
]),
|
||||
];
|
||||
accessStore.setAccessCodes(mergedAccessCodes);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ const gridOptions: VxeGridProps<AdminConfig> = {
|
||||
{ field: 'remark', title: '备注', },
|
||||
{title: '操作',width: 200,fixed: 'right',slots: {default:"operation"},},
|
||||
],
|
||||
height: 'auto',
|
||||
minHeight: 650,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
response: {
|
||||
|
||||
@ -44,7 +44,7 @@ const gridOptions: VxeGridProps<AdminLog> = {
|
||||
field: 'createTime', title: '时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'time_header' }
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
minHeight: 650,
|
||||
pagerConfig: {
|
||||
pageSize: 100,
|
||||
},
|
||||
|
||||
@ -139,7 +139,8 @@ const gridOptions: VxeGridProps<PermissionApi.Permission> = {
|
||||
},
|
||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
||||
],
|
||||
height: 'auto',
|
||||
minHeight: 650,
|
||||
autoResize: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
response: { result: 'items', total: 'total' },
|
||||
|
||||
@ -120,7 +120,7 @@ const gridOptions: VxeGridProps<PermissionApi.Role> = {
|
||||
{ field: 'remark', title: '备注', minWidth: 200 },
|
||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
||||
],
|
||||
height: 'auto',
|
||||
minHeight: 650,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
response: { result: 'items', total: 'total' },
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
getUserGroupsApi,
|
||||
setUserGroupsApi,
|
||||
getUserRolesApi,
|
||||
getUserRolesBatchApi,
|
||||
getPermissionListApi,
|
||||
getUserPermissionsDirectApi,
|
||||
setUserPermissionsDirectApi,
|
||||
@ -18,9 +19,10 @@ import {
|
||||
Button, Card, Space, Tag, Spin, message,
|
||||
Modal, Tabs, TabPane, Transfer,
|
||||
} from 'ant-design-vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed, onActivated, ref } from 'vue';
|
||||
|
||||
// ─── 行级缓存(用户组、角色)─────────────────────────────
|
||||
const currentUsers = ref<UserInfo[]>([]);
|
||||
const userGroupsMap = ref<Map<number, PermissionApi.UserGroup[]>>(new Map());
|
||||
const userRolesMap = ref<Map<number, PermissionApi.Role[]>>(new Map());
|
||||
|
||||
@ -28,18 +30,27 @@ async function loadRowDataForList(users: UserInfo[]) {
|
||||
const ids = users.map((u) => u.id).filter((id): id is number => id != null);
|
||||
const [groupResults, roleResults] = await Promise.all([
|
||||
Promise.allSettled(ids.map((id) => getUserGroupsApi(id))),
|
||||
Promise.allSettled(ids.map((id) => getUserRolesApi(id))),
|
||||
getUserRolesBatchApi(ids),
|
||||
]);
|
||||
const gMap = new Map<number, PermissionApi.UserGroup[]>();
|
||||
const rMap = new Map<number, PermissionApi.Role[]>();
|
||||
ids.forEach((id, i) => {
|
||||
if (groupResults[i]!.status === 'fulfilled') gMap.set(id, (groupResults[i] as any).value || []);
|
||||
if (roleResults[i]!.status === 'fulfilled') rMap.set(id, (roleResults[i] as any).value || []);
|
||||
rMap.set(id, roleResults?.[String(id)] || []);
|
||||
});
|
||||
userGroupsMap.value = gMap;
|
||||
userRolesMap.value = rMap;
|
||||
}
|
||||
|
||||
async function refreshUserRelationCache(adminId: number) {
|
||||
const [groups, roles] = await Promise.all([
|
||||
getUserGroupsApi(adminId),
|
||||
getUserRolesApi(adminId),
|
||||
]);
|
||||
userGroupsMap.value = new Map(userGroupsMap.value).set(adminId, groups || []);
|
||||
userRolesMap.value = new Map(userRolesMap.value).set(adminId, roles || []);
|
||||
}
|
||||
|
||||
// ─── 分配弹窗 ─────────────────────────────────────────────
|
||||
const modalOpen = ref(false);
|
||||
const modalLoading = ref(false);
|
||||
@ -82,15 +93,19 @@ async function openAssignModal(row: UserInfo) {
|
||||
modalLoading.value = true;
|
||||
modalOpen.value = true;
|
||||
try {
|
||||
const [groupsRes, permsRes, userGroups, userDirectPerms] = await Promise.all([
|
||||
const [groupsRes, permsRes, userGroups, userDirectPerms, userRoles] = await Promise.all([
|
||||
getUserGroupListApi({ page: 1, pageSize: 500 }),
|
||||
getPermissionListApi({ page: 1, pageSize: 500 }),
|
||||
getUserGroupsApi(row.id),
|
||||
getUserPermissionsDirectApi(row.id),
|
||||
getUserRolesApi(row.id),
|
||||
]);
|
||||
console.log('userGroups', userGroups);
|
||||
allGroups.value = groupsRes.items || [];
|
||||
allPermissions.value = permsRes.items || [];
|
||||
selectedGroupIds.value = (userGroups || []).map((g) => String(g.id));
|
||||
userGroupsMap.value = new Map(userGroupsMap.value).set(row.id, userGroups || []);
|
||||
userRolesMap.value = new Map(userRolesMap.value).set(row.id, userRoles || []);
|
||||
const directPerms = userDirectPerms || [];
|
||||
selectedAllowPermIds.value = directPerms.filter((p) => p.grant_type === 1).map((p) => String(p.permission_id));
|
||||
selectedDenyPermIds.value = directPerms.filter((p) => p.grant_type === 2).map((p) => String(p.permission_id));
|
||||
@ -113,13 +128,7 @@ async function handleConfirm() {
|
||||
]);
|
||||
message.success('权限分配成功');
|
||||
modalOpen.value = false;
|
||||
// 刷新当前行缓存
|
||||
const [groups, roles] = await Promise.all([
|
||||
getUserGroupsApi(currentAdminId.value),
|
||||
getUserRolesApi(currentAdminId.value),
|
||||
]);
|
||||
userGroupsMap.value = new Map(userGroupsMap.value).set(currentAdminId.value, groups || []);
|
||||
userRolesMap.value = new Map(userRolesMap.value).set(currentAdminId.value, roles || []);
|
||||
await refreshUserRelationCache(currentAdminId.value);
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
@ -136,7 +145,7 @@ const gridOptions: VxeGridProps<UserInfo> = {
|
||||
{ field: 'userRoles', title: '权限组(角色)', minWidth: 200, slots: { default: 'userRolesSlot' } },
|
||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 120 },
|
||||
],
|
||||
height: '100%',
|
||||
minHeight: 650,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
response: { total: 'total', result: 'data' },
|
||||
@ -144,6 +153,7 @@ const gridOptions: VxeGridProps<UserInfo> = {
|
||||
query: async () => {
|
||||
const result = await getAdminListApi();
|
||||
const list: UserInfo[] = Array.isArray(result) ? result : ((result as any)?.data ?? []);
|
||||
currentUsers.value = list;
|
||||
void loadRowDataForList(list);
|
||||
return result;
|
||||
},
|
||||
@ -153,6 +163,12 @@ const gridOptions: VxeGridProps<UserInfo> = {
|
||||
};
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
onActivated(() => {
|
||||
if (currentUsers.value.length > 0) {
|
||||
void loadRowDataForList(currentUsers.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -140,7 +140,7 @@ const gridOptions: VxeGridProps<PermissionApi.UserGroup> = {
|
||||
{ field: 'remark', title: '备注', minWidth: 200 },
|
||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
||||
],
|
||||
height: 'auto',
|
||||
minHeight: 650,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
response: { result: 'items', total: 'total' },
|
||||
|
||||
@ -1,62 +1,279 @@
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { Button, Card, Space } from 'ant-design-vue';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { useVbenModal, useVbenForm, Page } from '@vben/common-ui';
|
||||
import { Button, Card, Space, Tag, Modal, message } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import type { UserInfo } from '#/model/admin.user';
|
||||
import { getAdminListApi } from '#/api/core/admin.user';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import addUserModal from './addUser.vue';
|
||||
import {
|
||||
getAdminListApi,
|
||||
addAdminApi,
|
||||
editAdminApi,
|
||||
deleteAdminApi,
|
||||
} from '#/api/core/admin.user';
|
||||
|
||||
const ROLE_MAP: Record<number, { label: string; color: string }> = {
|
||||
0: { label: '超级管理员', color: 'red' },
|
||||
1: { label: '管理员', color: 'blue' },
|
||||
2: { label: '普通用户', color: 'green' },
|
||||
99: { label: '外包翻译', color: 'orange' },
|
||||
};
|
||||
|
||||
const editingId = ref<number | undefined>(undefined);
|
||||
|
||||
const [Form, FormApi] = useVbenForm({
|
||||
layout: 'horizontal',
|
||||
wrapperClass: 'grid-cols-2',
|
||||
showDefaultActions: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
formItemClass: 'col-span-1',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '登录用户名' },
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
formItemClass: 'col-span-1',
|
||||
componentProps: { placeholder: '留空则不修改密码' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'real_name',
|
||||
label: '真实姓名',
|
||||
formItemClass: 'col-span-1',
|
||||
componentProps: { placeholder: '真实姓名' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'nickname',
|
||||
label: '昵称',
|
||||
formItemClass: 'col-span-1',
|
||||
componentProps: { placeholder: '昵称' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'phone',
|
||||
label: '手机号',
|
||||
formItemClass: 'col-span-1',
|
||||
componentProps: { placeholder: '手机号' },
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
formItemClass: 'col-span-1',
|
||||
componentProps: { placeholder: '邮箱地址' },
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'role',
|
||||
label: '角色',
|
||||
formItemClass: 'col-span-1',
|
||||
defaultValue: 1,
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '超级管理员', value: 0 },
|
||||
{ label: '管理员', value: 1 },
|
||||
{ label: '普通用户', value: 2 },
|
||||
{ label: '外包翻译', value: 99 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
formItemClass: 'col-span-1',
|
||||
defaultValue: 1,
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'group',
|
||||
label: '用户组',
|
||||
formItemClass: 'col-span-2',
|
||||
componentProps: { placeholder: '用户组标识(可选)' },
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
formItemClass: 'col-span-2',
|
||||
componentProps: { rows: 3 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [FormModal, FormModalApi] = useVbenModal({
|
||||
confirmText: '保存',
|
||||
onConfirm: async () => {
|
||||
const { valid } = await FormApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await FormApi.getValues();
|
||||
const params: UserInfo = {
|
||||
id: editingId.value,
|
||||
username: values.username,
|
||||
password: values.password || '',
|
||||
real_name: values.real_name,
|
||||
nickname: values.nickname,
|
||||
phone: values.phone,
|
||||
email: values.email,
|
||||
role: values.role,
|
||||
status: values.status,
|
||||
group: values.group,
|
||||
remark: values.remark,
|
||||
};
|
||||
if (editingId.value) {
|
||||
await editAdminApi(params);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
if (!params.password) {
|
||||
message.error('新增用户必须填写密码');
|
||||
return;
|
||||
}
|
||||
await addAdminApi(params);
|
||||
message.success('新增成功');
|
||||
}
|
||||
FormModalApi.close();
|
||||
GridApi.reload();
|
||||
},
|
||||
});
|
||||
|
||||
const gridOptions: VxeGridProps<UserInfo> = {
|
||||
columns: [
|
||||
{ field: 'username', title: '用户名', },
|
||||
// { field: 'phone', title: '手机号' },
|
||||
{ field: 'role', title: '角色' },
|
||||
{ field: 'group', title: '用户组' },
|
||||
{ field: 'remark', title: '备注' },
|
||||
{ field: 'id', title: 'ID', width: 60 },
|
||||
{ field: 'username', title: '用户名', width: 120 },
|
||||
{ field: 'real_name', title: '真实姓名', width: 110 },
|
||||
{ field: 'nickname', title: '昵称', width: 110 },
|
||||
{ field: 'phone', title: '手机号', width: 130 },
|
||||
{
|
||||
field: 'role',
|
||||
title: '角色',
|
||||
width: 120,
|
||||
slots: { default: 'roleSlot' },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 90,
|
||||
slots: { default: 'statusSlot' },
|
||||
},
|
||||
{
|
||||
field: 'lastLoginTime',
|
||||
title: '最后登录',
|
||||
minWidth: 160,
|
||||
slots: { default: 'loginTimeSlot' },
|
||||
},
|
||||
{ field: 'lastLoginIp', title: '登录IP', width: 130 },
|
||||
{ field: 'remark', title: '备注', minWidth: 120 },
|
||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
||||
],
|
||||
height: 'auto',
|
||||
minHeight: 650,
|
||||
autoResize: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
response: {
|
||||
total: 'total',
|
||||
result: 'data',
|
||||
},
|
||||
response: { result: 'data', total: 'total' },
|
||||
ajax: {
|
||||
query: async () => {
|
||||
return await getAdminListApi();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
},
|
||||
rowConfig: { isHover: true },
|
||||
};
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
|
||||
const [addUserM, addUserApi] = useVbenModal({
|
||||
connectedComponent: addUserModal,
|
||||
onClosed: async () => {
|
||||
addUserApi.close();
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = undefined;
|
||||
FormApi.resetForm();
|
||||
FormModalApi.setState({ title: '新增管理员' });
|
||||
FormModalApi.open();
|
||||
}
|
||||
|
||||
function openEdit(row: UserInfo) {
|
||||
editingId.value = row.id;
|
||||
FormApi.setValues({
|
||||
username: row.username,
|
||||
password: '',
|
||||
real_name: row.real_name,
|
||||
nickname: row.nickname,
|
||||
phone: row.phone,
|
||||
email: row.email,
|
||||
role: row.role,
|
||||
status: row.status,
|
||||
group: row.group,
|
||||
remark: row.remark,
|
||||
});
|
||||
FormModalApi.setState({ title: `编辑管理员 - ${row.username}` });
|
||||
FormModalApi.open();
|
||||
}
|
||||
|
||||
function handleDelete(row: UserInfo) {
|
||||
Modal.confirm({
|
||||
title: '删除确认',
|
||||
content: `确认删除管理员「${row.username}」吗?此操作不可撤销。`,
|
||||
okType: 'danger',
|
||||
async onOk() {
|
||||
await deleteAdminApi(row.id!);
|
||||
message.success('删除成功');
|
||||
GridApi.reload();
|
||||
},
|
||||
});
|
||||
const addAdmin = () => {
|
||||
addUserApi.open();
|
||||
};
|
||||
}
|
||||
|
||||
function formatTime(ts: number | undefined) {
|
||||
if (!ts) return '-';
|
||||
return dayjs.unix(ts).format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<addUserM class="w-[50%]" />
|
||||
<Card class="mb-5" title="用户操作">
|
||||
<Space>
|
||||
<Button @click="addAdmin">新增</Button>
|
||||
<Button> 删除 </Button>
|
||||
</Space>
|
||||
<FormModal>
|
||||
<Form />
|
||||
</FormModal>
|
||||
|
||||
<Card class="mb-4">
|
||||
<template #title>管理员列表</template>
|
||||
<template #extra>
|
||||
<Button type="primary" @click="openCreate">新增管理员</Button>
|
||||
</template>
|
||||
</Card>
|
||||
<Grid />
|
||||
|
||||
<Grid>
|
||||
<template #roleSlot="{ row }">
|
||||
<Tag :color="ROLE_MAP[row.role]?.color ?? 'default'">
|
||||
{{ ROLE_MAP[row.role]?.label ?? `角色${row.role}` }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #statusSlot="{ row }">
|
||||
<Tag :color="row.status === 1 ? 'green' : 'red'">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #loginTimeSlot="{ row }">
|
||||
{{ formatTime(row.lastLoginTime) }}
|
||||
</template>
|
||||
<template #actionSlot="{ row }">
|
||||
<Space>
|
||||
<Button size="small" type="primary" @click="openEdit(row)">编辑</Button>
|
||||
<Button danger size="small" @click="handleDelete(row)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@ -29,15 +29,7 @@ onMounted(async () => {
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: data.value2,
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
name: '日活',
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
@ -51,14 +43,6 @@ onMounted(async () => {
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
@ -79,7 +63,6 @@ onMounted(async () => {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 5_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { getstatisticsHeat } from '#/api/core/statistics';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
@ -10,7 +11,8 @@ import {
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const data = await getstatisticsHeat({ AppId: 0 });
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
@ -22,29 +24,24 @@ onMounted(() => {
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
data: data.value2,
|
||||
name: '注册人数',
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
data: data.key,
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
|
||||
@ -3,10 +3,6 @@ import { onMounted, ref } from 'vue';
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
import { getstatisticsInfo } from '#/api/core/statistics';
|
||||
import { AccessControl } from '@vben/access';
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
const { hasAccessByRoles } = useAccess();
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
@ -77,11 +73,11 @@ onMounted(async () => {
|
||||
});
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '热度趋势',
|
||||
label: '日活',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
label: '注册人数',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
@ -89,7 +85,6 @@ const chartTabs: TabOption[] = [
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AccessControl :codes="['super', 'admin']" type="role">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
@ -99,10 +94,9 @@ const chartTabs: TabOption[] = [
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<!-- <div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
@ -112,7 +106,6 @@ const chartTabs: TabOption[] = [
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</AccessControl>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -72,7 +72,7 @@ const gridOptions: VxeGridProps<ActivityData> = {
|
||||
{ field: 'interval', title: '活动循环间隔(秒)从上次开始时间开始计算,0表示不循环', },
|
||||
{ field: 'tag', title: '状态', slots: { default: 'tag' } },
|
||||
],
|
||||
height: 'auto',
|
||||
minHeight: '650px',
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
response: {
|
||||
@ -83,7 +83,6 @@ const gridOptions: VxeGridProps<ActivityData> = {
|
||||
query: async ({ page }, formValues) => {
|
||||
let AppId = parseNumber(formValues.AppId);
|
||||
let activityType = parseNumber(formValues.activityType);
|
||||
console.log('query', formValues, page);
|
||||
const response = await getActivityListApi({
|
||||
AppId: AppId,
|
||||
ServerId: formValues.ServerId,
|
||||
@ -125,6 +124,8 @@ const gridOptions: VxeGridProps<ActivityData> = {
|
||||
item.tag = '生效中';
|
||||
}
|
||||
}
|
||||
console.log(response);
|
||||
|
||||
return response;
|
||||
},
|
||||
},
|
||||
|
||||
182
apps/web-antd/src/views/operation/apk/index.vue
Normal file
182
apps/web-antd/src/views/operation/apk/index.vue
Normal file
@ -0,0 +1,182 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Empty, Space, Tag, TypographyParagraph, message } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { getApkPackagesApi } from '#/api/core/apk';
|
||||
import type { ApkApi } from '#/api/core/apk';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const downloadingEnv = ref<ApkApi.Environment | ''>('');
|
||||
const packages = ref<ApkApi.PackageItem[]>([]);
|
||||
|
||||
const envLabelMap: Record<ApkApi.Environment, string> = {
|
||||
dev: '开发环境',
|
||||
stable: '稳定环境',
|
||||
prod: '正式环境',
|
||||
};
|
||||
|
||||
const envDescriptionMap: Record<ApkApi.Environment, string> = {
|
||||
dev: '用于日常开发和联调验证。',
|
||||
stable: '用于提测、回归和灰度验证。',
|
||||
prod: '用于正式对外发布。',
|
||||
};
|
||||
|
||||
async function loadPackages() {
|
||||
loading.value = true;
|
||||
try {
|
||||
packages.value = await getApkPackagesApi();
|
||||
} catch {
|
||||
packages.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(size: number) {
|
||||
if (!size) {
|
||||
return '-';
|
||||
}
|
||||
if (size < 1024 * 1024) {
|
||||
return `${(size / 1024).toFixed(2)} KB`;
|
||||
}
|
||||
return `${(size / 1024 / 1024).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
function formatTime(value: string) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
function resolveDownloadUrl(env: ApkApi.Environment) {
|
||||
return `${apiURL.replace(/\/$/, '')}/apk/download/${env}`;
|
||||
}
|
||||
|
||||
function resolveDownloadName(item: ApkApi.PackageItem, disposition: null | string) {
|
||||
const fallbackName = item.fileName || `${item.env}.apk`;
|
||||
if (!disposition) {
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
if (utf8Match?.[1]) {
|
||||
return decodeURIComponent(utf8Match[1]);
|
||||
}
|
||||
|
||||
const basicMatch = disposition.match(/filename="?([^";]+)"?/i);
|
||||
if (basicMatch?.[1]) {
|
||||
return decodeURIComponent(basicMatch[1]);
|
||||
}
|
||||
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
async function downloadPackage(item: ApkApi.PackageItem) {
|
||||
if (!item.exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accessStore.accessToken) {
|
||||
message.error('登录状态已失效,请重新登录后再试。');
|
||||
return;
|
||||
}
|
||||
|
||||
downloadingEnv.value = item.env;
|
||||
try {
|
||||
const response = await fetch(resolveDownloadUrl(item.env), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('下载失败');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const downloadName = resolveDownloadName(
|
||||
item,
|
||||
response.headers.get('content-disposition'),
|
||||
);
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = downloadUrl;
|
||||
anchor.download = downloadName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
message.success(`开始下载 ${downloadName}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error(`${envLabelMap[item.env]} APK 下载失败`);
|
||||
} finally {
|
||||
downloadingEnv.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPackages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="space-y-4 p-4">
|
||||
<Card title="客户端下载说明">
|
||||
<TypographyParagraph>
|
||||
当前页面展示 dev、stable、prod 三套客户端 APK 包。
|
||||
Jenkins 通过后端上传接口更新包后,这里会自动展示最新版本并支持后台下载。
|
||||
</TypographyParagraph>
|
||||
<Button :loading="loading" type="primary" @click="loadPackages">刷新列表</Button>
|
||||
</Card>
|
||||
|
||||
<div v-if="packages.length" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card v-for="item in packages" :key="item.env" :title="envLabelMap[item.env]">
|
||||
<Space wrap>
|
||||
<Tag :color="item.exists ? 'green' : 'default'">
|
||||
{{ item.exists ? '已上传' : '暂无包' }}
|
||||
</Tag>
|
||||
<Tag color="blue">{{ item.env }}</Tag>
|
||||
</Space>
|
||||
|
||||
<TypographyParagraph class="mt-4 text-gray-500">
|
||||
{{ envDescriptionMap[item.env] }}
|
||||
</TypographyParagraph>
|
||||
|
||||
<div class="space-y-2 text-sm leading-6">
|
||||
<div>文件名:{{ item.fileName || '-' }}</div>
|
||||
<div>版本号:{{ item.version || '-' }}</div>
|
||||
<div>文件大小:{{ formatSize(item.size) }}</div>
|
||||
<div>上传时间:{{ formatTime(item.uploadedAt) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
:disabled="!item.exists"
|
||||
:loading="downloadingEnv === item.env"
|
||||
@click="downloadPackage(item)"
|
||||
>
|
||||
下载 APK
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card v-else>
|
||||
<Empty description="暂无可展示的 APK 包信息" />
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@ -16,7 +16,7 @@ import type { Order, Merge, Chess, friendRecord } from '#/model/type';
|
||||
import dayjs from 'dayjs';
|
||||
import { WorkbenchDetail } from '@vben/common-ui';
|
||||
import UserHeader from './user-header.vue';
|
||||
import { AccessControl } from '@vben/access';
|
||||
import { AccessControl, useAccess } from '@vben/access';
|
||||
import eventTable from './event-table.vue';
|
||||
import assetTable from './asset-table.vue';
|
||||
import orderTable from './order-table.vue';
|
||||
@ -27,6 +27,14 @@ import orderTable from './order-table.vue';
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [];
|
||||
|
||||
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
|
||||
const canUseGm = computed(() =>
|
||||
hasAccessByRoles(['super']) || hasAccessByCodes(['AC3003']),
|
||||
);
|
||||
const canBanUser = computed(() =>
|
||||
hasAccessByRoles(['super']) || hasAccessByCodes(['AC3004']),
|
||||
);
|
||||
|
||||
const chargeDisplay = computed(() => (Number(info.value?.Charge ?? 0)).toFixed(2));
|
||||
|
||||
const [BaseForm] = useVbenForm({
|
||||
@ -268,6 +276,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
data.value = modalApi.getData<Record<string, any>>();
|
||||
const b = hasAccessByCodes(['AC3003']);
|
||||
console.log('canUseGm', canUseGm.value, 'hasAccessByCodes AC3003', b);
|
||||
try {
|
||||
const r = await getUserlogInfoApi({
|
||||
Id: data.value.uid,
|
||||
@ -476,16 +486,18 @@ function formatActLog(type: number, content = ''): [string, string] {
|
||||
<template #energy>{{ info.Energy }} </template>
|
||||
<template #diamond>{{ info.Diamond }}</template>
|
||||
</UserHeader>
|
||||
<AccessControl :codes="['super', 'admin']" type="role">
|
||||
<div class="mt-5 flex">
|
||||
<AccessControl :codes="['AC3003']" type="code">
|
||||
<Card class="card-box flex flex-col p-5 w-[50%]">
|
||||
<BaseForm />
|
||||
</Card>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['AC3004']" type="code">
|
||||
<Card class="card-box flex flex-col p-5 w-[45%] ml-5">
|
||||
<BanForm />
|
||||
</Card>
|
||||
</div>
|
||||
</AccessControl>
|
||||
</div>
|
||||
<LoginHeatmap :app-id="data?.AppId" :uid="data?.uid" title="登录热力图" class="mt-5" />
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
|
||||
@ -15,6 +15,11 @@ interface UserInfo extends BasicUserInfo {
|
||||
* accessToken
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* 用户有效单点权限码列表(由后端 /user/info 返回)
|
||||
*/
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export type { UserInfo };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user