权限管理、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);
|
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) {
|
export async function getAdminConfigList(param:AdminConfigListParam) {
|
||||||
return requestClient.post<AdminConfig>('/admin/config/list', param);
|
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) {
|
export async function getUserRolesApi(admin_id: number) {
|
||||||
return requestClient.post<PermissionApi.Role[]>('/admin/user/role/list', { admin_id });
|
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": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics"
|
||||||
|
},
|
||||||
|
"devops": {
|
||||||
|
"title": "DevOps",
|
||||||
"server-list": "Server List",
|
"server-list": "Server List",
|
||||||
"node-list": "Node List",
|
"node-list": "Node List",
|
||||||
"mysql-list": "MySQL List",
|
"mysql-list": "MySQL List",
|
||||||
@ -65,6 +68,7 @@
|
|||||||
},
|
},
|
||||||
"operation": {
|
"operation": {
|
||||||
"title": "Operation",
|
"title": "Operation",
|
||||||
|
"apk": "Client APK",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"mail": "Mail",
|
"mail": "Mail",
|
||||||
"order": "Order",
|
"order": "Order",
|
||||||
|
|||||||
@ -40,8 +40,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
|
"title": "工作台",
|
||||||
|
"analytics": "分析台"
|
||||||
|
},
|
||||||
|
"devops": {
|
||||||
"title": "运维管理",
|
"title": "运维管理",
|
||||||
"analytics": "分析台",
|
|
||||||
"server-list": "区服列表",
|
"server-list": "区服列表",
|
||||||
"node-list": "节点列表",
|
"node-list": "节点列表",
|
||||||
"mysql-list": "MySQL列表",
|
"mysql-list": "MySQL列表",
|
||||||
@ -65,6 +68,7 @@
|
|||||||
},
|
},
|
||||||
"operation": {
|
"operation": {
|
||||||
"title": "运营管理",
|
"title": "运营管理",
|
||||||
|
"apk": "客户端 APK 下载",
|
||||||
"scripts": "自动化脚本",
|
"scripts": "自动化脚本",
|
||||||
"mail": "邮件管理",
|
"mail": "邮件管理",
|
||||||
"order": "订单管理",
|
"order": "订单管理",
|
||||||
|
|||||||
@ -2,12 +2,18 @@ export interface UserInfo {
|
|||||||
id?: number;
|
id?: number;
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
phone: string;
|
real_name?: string;
|
||||||
|
nickname?: string;
|
||||||
|
phone?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
uid?: string;
|
group?: string;
|
||||||
group: string;
|
|
||||||
role: number;
|
role: number;
|
||||||
|
status?: number;
|
||||||
|
lastLoginTime?: number;
|
||||||
|
lastLoginIp?: string;
|
||||||
|
createTime?: number;
|
||||||
|
updateTime?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,13 +50,24 @@ function setupAccessGuard(router: Router) {
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
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 (coreRouteNames.includes(to.name as string)) {
|
||||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||||
|
const homePath = await getResolvedHomePath();
|
||||||
return decodeURIComponent(
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,10 +100,13 @@ function setupAccessGuard(router: Router) {
|
|||||||
// 当前登录用户拥有的角色标识列表
|
// 当前登录用户拥有的角色标识列表
|
||||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||||
const userRoles = userInfo.roles ?? [];
|
const userRoles = userInfo.roles ?? [];
|
||||||
|
// 将单点权限码合并进 roles,供路由 authority 字段做精细匹配
|
||||||
|
const userPermissions: string[] = (userInfo as any).permissions ?? [];
|
||||||
|
const accessIdentifiers = [...new Set([...userRoles, ...userPermissions])];
|
||||||
|
|
||||||
// 生成菜单和路由
|
// 生成菜单和路由
|
||||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||||
roles: userRoles,
|
roles: accessIdentifiers,
|
||||||
router,
|
router,
|
||||||
// 则会在菜单中显示,但是访问会被重定向到403
|
// 则会在菜单中显示,但是访问会被重定向到403
|
||||||
routes: accessRoutes,
|
routes: accessRoutes,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
icon: 'material-symbols:brightness-empty-outline',
|
icon: 'material-symbols:brightness-empty-outline',
|
||||||
order: -1,
|
order: -1,
|
||||||
title: $t('page.admin.title'),
|
title: $t('page.admin.title'),
|
||||||
authority:['super'],
|
authority: ['super', 'AC1001', 'AC1101', 'AC1107', 'AC1113', 'AC1117'],
|
||||||
},
|
},
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
@ -20,7 +20,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/user-management',
|
path: '/user-management',
|
||||||
component: () => import('#/views/admin/user/index.vue'),
|
component: () => import('#/views/admin/user/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC1001', 'AC1002'],
|
||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'majesticons:user-box-line',
|
icon: 'majesticons:user-box-line',
|
||||||
title: $t('page.admin.user'),
|
title: $t('page.admin.user'),
|
||||||
@ -31,7 +31,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/user-management-log',
|
path: '/user-management-log',
|
||||||
component: () => import('#/views/admin/log/index.vue'),
|
component: () => import('#/views/admin/log/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC1003'],
|
||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'material-symbols:assignment-rounded',
|
icon: 'material-symbols:assignment-rounded',
|
||||||
title: $t('page.admin.log'),
|
title: $t('page.admin.log'),
|
||||||
@ -42,7 +42,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/user-management-config',
|
path: '/user-management-config',
|
||||||
component: () => import('#/views/admin/config/index.vue'),
|
component: () => import('#/views/admin/config/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC1004', 'AC1005', 'AC1006'],
|
||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'material-symbols:assignment-rounded',
|
icon: 'material-symbols:assignment-rounded',
|
||||||
title: $t('page.admin.config'),
|
title: $t('page.admin.config'),
|
||||||
@ -53,7 +53,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/permission/user-group',
|
path: '/permission/user-group',
|
||||||
component: () => import('#/views/admin/permission/user-group.vue'),
|
component: () => import('#/views/admin/permission/user-group.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['super'],
|
authority: ['super', 'AC1101', 'AC1102', 'AC1103', 'AC1104', 'AC1105', 'AC1106'],
|
||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'material-symbols:group',
|
icon: 'material-symbols:group',
|
||||||
title: $t('page.admin.permission.userGroup'),
|
title: $t('page.admin.permission.userGroup'),
|
||||||
@ -64,7 +64,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/permission/role',
|
path: '/permission/role',
|
||||||
component: () => import('#/views/admin/permission/role.vue'),
|
component: () => import('#/views/admin/permission/role.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['super'],
|
authority: ['super', 'AC1107', 'AC1108', 'AC1109', 'AC1110', 'AC1111', 'AC1112'],
|
||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'material-symbols:shield-person',
|
icon: 'material-symbols:shield-person',
|
||||||
title: $t('page.admin.permission.role'),
|
title: $t('page.admin.permission.role'),
|
||||||
@ -75,7 +75,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/permission/permission',
|
path: '/permission/permission',
|
||||||
component: () => import('#/views/admin/permission/permission.vue'),
|
component: () => import('#/views/admin/permission/permission.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['super'],
|
authority: ['super', 'AC1113', 'AC1114', 'AC1115', 'AC1116'],
|
||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'material-symbols:key',
|
icon: 'material-symbols:key',
|
||||||
title: $t('page.admin.permission.permission'),
|
title: $t('page.admin.permission.permission'),
|
||||||
@ -86,7 +86,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/permission/user-assign',
|
path: '/permission/user-assign',
|
||||||
component: () => import('#/views/admin/permission/user-assign.vue'),
|
component: () => import('#/views/admin/permission/user-assign.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
authority: ['super'],
|
authority: ['super', 'AC1117', 'AC1118', 'AC1119', 'AC1120', 'AC1121'],
|
||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'material-symbols:manage-accounts',
|
icon: 'material-symbols:manage-accounts',
|
||||||
title: $t('page.admin.permission.userAssign'),
|
title: $t('page.admin.permission.userAssign'),
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
component: BasicLayout,
|
component: BasicLayout,
|
||||||
meta: {
|
meta: {
|
||||||
|
authority: ['super', 'AC5003', 'AC5004'],
|
||||||
icon: 'lucide:layout-dashboard',
|
icon: 'lucide:layout-dashboard',
|
||||||
order: -1,
|
order: -1,
|
||||||
title: $t('page.dashboard.title'),
|
title: $t('page.dashboard.title'),
|
||||||
@ -19,56 +20,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/analytics',
|
path: '/analytics',
|
||||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
affixTab: false,
|
affixTab: true,
|
||||||
|
authority: ['super', 'AC5003', 'AC5004'],
|
||||||
icon: 'lucide:area-chart',
|
icon: 'lucide:area-chart',
|
||||||
title: $t('page.dashboard.analytics'),
|
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',
|
icon: 'lucide:layout-dashboard',
|
||||||
order: -1,
|
order: -1,
|
||||||
title: $t('page.experiment.title'),
|
title: $t('page.experiment.title'),
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC9201', 'AC9202', 'AC9214'],
|
||||||
},
|
},
|
||||||
name: 'Experiment',
|
name: 'Experiment',
|
||||||
path: '/experiment',
|
path: '/experiment',
|
||||||
@ -23,6 +23,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: false,
|
affixTab: false,
|
||||||
icon: 'lucide:flask-conical',
|
icon: 'lucide:flask-conical',
|
||||||
title: $t('page.experiment.abtest'),
|
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,
|
affixTab: false,
|
||||||
icon: 'lucide:users',
|
icon: 'lucide:users',
|
||||||
title: $t('page.experiment.groupQuery'),
|
title: $t('page.experiment.groupQuery'),
|
||||||
|
authority: ['super', 'AC9214'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
icon: 'solar:book-2-bold',
|
icon: 'solar:book-2-bold',
|
||||||
order: 1001,
|
order: 1001,
|
||||||
title: $t('page.language.title'),
|
title: $t('page.language.title'),
|
||||||
|
authority: ['super', 'AC9001', 'AC9002', 'AC9003', 'AC9004', 'AC9005'],
|
||||||
},
|
},
|
||||||
name: 'Translate',
|
name: 'Translate',
|
||||||
path: '/translate',
|
path: '/translate',
|
||||||
@ -22,6 +23,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lets-icons:order',
|
icon: 'lets-icons:order',
|
||||||
title: $t('page.language.translationList'),
|
title: $t('page.language.translationList'),
|
||||||
|
authority: ['super', 'AC9001', 'AC9002', 'AC9003', 'AC9004', 'AC9005'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
icon: 'lucide:bell-ring',
|
icon: 'lucide:bell-ring',
|
||||||
order: 1002,
|
order: 1002,
|
||||||
title: $t('page.notification.title'),
|
title: $t('page.notification.title'),
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC9101', 'AC9102'],
|
||||||
},
|
},
|
||||||
name: 'Notification',
|
name: 'Notification',
|
||||||
path: '/notification',
|
path: '/notification',
|
||||||
@ -23,7 +23,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lucide:bell-plus',
|
icon: 'lucide:bell-plus',
|
||||||
title: $t('page.notification.config'),
|
title: $t('page.notification.config'),
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC9101', 'AC9102'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -10,11 +10,22 @@ const routes: RouteRecordRaw[] = [
|
|||||||
icon: 'lucide:file-clock',
|
icon: 'lucide:file-clock',
|
||||||
order: 1001,
|
order: 1001,
|
||||||
title: $t('page.operation.title'),
|
title: $t('page.operation.title'),
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC6001', 'AC7001', 'AC8001', 'AC9301', 'AC5002','AC9401'],
|
||||||
},
|
},
|
||||||
name: 'Operation',
|
name: 'Operation',
|
||||||
path: '/operation',
|
path: '/operation',
|
||||||
children: [
|
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',
|
name: 'Scripts',
|
||||||
path: '/scripts',
|
path: '/scripts',
|
||||||
@ -23,7 +34,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lucide:chart-no-axes-column-increasing',
|
icon: 'lucide:chart-no-axes-column-increasing',
|
||||||
title: $t('page.operation.scripts'),
|
title: $t('page.operation.scripts'),
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC9301', 'AC9302', 'AC9303'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -34,6 +45,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lucide:mail',
|
icon: 'lucide:mail',
|
||||||
title: $t('page.operation.mail'),
|
title: $t('page.operation.mail'),
|
||||||
|
authority: ['super', 'AC7001', 'AC7002', 'AC7003'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -44,6 +56,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lucide:mail',
|
icon: 'lucide:mail',
|
||||||
title: $t('page.operation.copyUser'),
|
title: $t('page.operation.copyUser'),
|
||||||
|
authority: ['super', 'AC8001'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -54,6 +67,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lets-icons:order',
|
icon: 'lets-icons:order',
|
||||||
title: $t('page.operation.order'),
|
title: $t('page.operation.order'),
|
||||||
|
authority: ['super', 'AC5002'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -64,6 +78,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lets-icons:order',
|
icon: 'lets-icons:order',
|
||||||
title: $t('page.operation.activity'),
|
title: $t('page.operation.activity'),
|
||||||
|
authority: ['super', 'AC6001', 'AC6002', 'AC6003', 'AC6004', 'AC6005'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
icon: 'lucide:laugh',
|
icon: 'lucide:laugh',
|
||||||
order: 1000,
|
order: 1000,
|
||||||
title: $t('page.userlog.title'),
|
title: $t('page.userlog.title'),
|
||||||
authority: ['super', 'admin'],
|
authority: ['super', 'AC2001', 'AC2002', 'AC2003', 'AC2004', 'AC2005', 'AC3001', 'AC3003', 'AC3004'],
|
||||||
},
|
},
|
||||||
name: 'Userlog',
|
name: 'Userlog',
|
||||||
path: '/userlog',
|
path: '/userlog',
|
||||||
@ -23,6 +23,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
affixTab: true,
|
affixTab: true,
|
||||||
icon: 'lucide:list',
|
icon: 'lucide:list',
|
||||||
title: $t('page.userlog.userlist'),
|
title: $t('page.userlog.userlist'),
|
||||||
|
authority: ['super', 'AC3001', 'AC3002', 'AC3003', 'AC3004'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -46,7 +46,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
userInfo = fetchUserInfoResult;
|
userInfo = fetchUserInfoResult;
|
||||||
userStore.setUserInfo(userInfo);
|
userStore.setUserInfo(userInfo);
|
||||||
accessStore.setAccessCodes(accessCodes);
|
const mergedAccessCodes = [
|
||||||
|
...new Set([...(accessCodes || []), ...(((userInfo as any)?.permissions || []) as string[])]),
|
||||||
|
];
|
||||||
|
accessStore.setAccessCodes(mergedAccessCodes);
|
||||||
|
|
||||||
if (accessStore.loginExpired) {
|
if (accessStore.loginExpired) {
|
||||||
accessStore.setLoginExpired(false);
|
accessStore.setLoginExpired(false);
|
||||||
@ -96,7 +99,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
userInfo = fetchUserInfoResult;
|
userInfo = fetchUserInfoResult;
|
||||||
|
|
||||||
userStore.setUserInfo(userInfo);
|
userStore.setUserInfo(userInfo);
|
||||||
accessStore.setAccessCodes(accessCodes);
|
const mergedAccessCodes = [
|
||||||
|
...new Set([...(accessCodes || []), ...(((userInfo as any)?.permissions || []) as string[])]),
|
||||||
|
];
|
||||||
|
accessStore.setAccessCodes(mergedAccessCodes);
|
||||||
|
|
||||||
if (accessStore.loginExpired) {
|
if (accessStore.loginExpired) {
|
||||||
accessStore.setLoginExpired(false);
|
accessStore.setLoginExpired(false);
|
||||||
@ -147,6 +153,13 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
let userInfo: null | UserInfo = null;
|
let userInfo: null | UserInfo = null;
|
||||||
userInfo = await getUserInfoApi();
|
userInfo = await getUserInfoApi();
|
||||||
userStore.setUserInfo(userInfo);
|
userStore.setUserInfo(userInfo);
|
||||||
|
const mergedAccessCodes = [
|
||||||
|
...new Set([
|
||||||
|
...(accessStore.accessCodes || []),
|
||||||
|
...(((userInfo as any)?.permissions || []) as string[]),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
accessStore.setAccessCodes(mergedAccessCodes);
|
||||||
return userInfo;
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const gridOptions: VxeGridProps<AdminConfig> = {
|
|||||||
{ field: 'remark', title: '备注', },
|
{ field: 'remark', title: '备注', },
|
||||||
{title: '操作',width: 200,fixed: 'right',slots: {default:"operation"},},
|
{title: '操作',width: 200,fixed: 'right',slots: {default:"operation"},},
|
||||||
],
|
],
|
||||||
height: 'auto',
|
minHeight: 650,
|
||||||
pagerConfig: {},
|
pagerConfig: {},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const gridOptions: VxeGridProps<AdminLog> = {
|
|||||||
field: 'createTime', title: '时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'time_header' }
|
field: 'createTime', title: '时间', formatter: ({ cellValue }) => formatUTC8Time(cellValue), slots: { header: 'time_header' }
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
height: 'auto',
|
minHeight: 650,
|
||||||
pagerConfig: {
|
pagerConfig: {
|
||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -139,7 +139,8 @@ const gridOptions: VxeGridProps<PermissionApi.Permission> = {
|
|||||||
},
|
},
|
||||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
||||||
],
|
],
|
||||||
height: 'auto',
|
minHeight: 650,
|
||||||
|
autoResize: true,
|
||||||
pagerConfig: {},
|
pagerConfig: {},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
response: { result: 'items', total: 'total' },
|
response: { result: 'items', total: 'total' },
|
||||||
|
|||||||
@ -120,7 +120,7 @@ const gridOptions: VxeGridProps<PermissionApi.Role> = {
|
|||||||
{ field: 'remark', title: '备注', minWidth: 200 },
|
{ field: 'remark', title: '备注', minWidth: 200 },
|
||||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
||||||
],
|
],
|
||||||
height: 'auto',
|
minHeight: 650,
|
||||||
pagerConfig: {},
|
pagerConfig: {},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
response: { result: 'items', total: 'total' },
|
response: { result: 'items', total: 'total' },
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
getUserGroupsApi,
|
getUserGroupsApi,
|
||||||
setUserGroupsApi,
|
setUserGroupsApi,
|
||||||
getUserRolesApi,
|
getUserRolesApi,
|
||||||
|
getUserRolesBatchApi,
|
||||||
getPermissionListApi,
|
getPermissionListApi,
|
||||||
getUserPermissionsDirectApi,
|
getUserPermissionsDirectApi,
|
||||||
setUserPermissionsDirectApi,
|
setUserPermissionsDirectApi,
|
||||||
@ -18,9 +19,10 @@ import {
|
|||||||
Button, Card, Space, Tag, Spin, message,
|
Button, Card, Space, Tag, Spin, message,
|
||||||
Modal, Tabs, TabPane, Transfer,
|
Modal, Tabs, TabPane, Transfer,
|
||||||
} from 'ant-design-vue';
|
} 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 userGroupsMap = ref<Map<number, PermissionApi.UserGroup[]>>(new Map());
|
||||||
const userRolesMap = ref<Map<number, PermissionApi.Role[]>>(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 ids = users.map((u) => u.id).filter((id): id is number => id != null);
|
||||||
const [groupResults, roleResults] = await Promise.all([
|
const [groupResults, roleResults] = await Promise.all([
|
||||||
Promise.allSettled(ids.map((id) => getUserGroupsApi(id))),
|
Promise.allSettled(ids.map((id) => getUserGroupsApi(id))),
|
||||||
Promise.allSettled(ids.map((id) => getUserRolesApi(id))),
|
getUserRolesBatchApi(ids),
|
||||||
]);
|
]);
|
||||||
const gMap = new Map<number, PermissionApi.UserGroup[]>();
|
const gMap = new Map<number, PermissionApi.UserGroup[]>();
|
||||||
const rMap = new Map<number, PermissionApi.Role[]>();
|
const rMap = new Map<number, PermissionApi.Role[]>();
|
||||||
ids.forEach((id, i) => {
|
ids.forEach((id, i) => {
|
||||||
if (groupResults[i]!.status === 'fulfilled') gMap.set(id, (groupResults[i] as any).value || []);
|
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;
|
userGroupsMap.value = gMap;
|
||||||
userRolesMap.value = rMap;
|
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 modalOpen = ref(false);
|
||||||
const modalLoading = ref(false);
|
const modalLoading = ref(false);
|
||||||
@ -82,15 +93,19 @@ async function openAssignModal(row: UserInfo) {
|
|||||||
modalLoading.value = true;
|
modalLoading.value = true;
|
||||||
modalOpen.value = true;
|
modalOpen.value = true;
|
||||||
try {
|
try {
|
||||||
const [groupsRes, permsRes, userGroups, userDirectPerms] = await Promise.all([
|
const [groupsRes, permsRes, userGroups, userDirectPerms, userRoles] = await Promise.all([
|
||||||
getUserGroupListApi({ page: 1, pageSize: 500 }),
|
getUserGroupListApi({ page: 1, pageSize: 500 }),
|
||||||
getPermissionListApi({ page: 1, pageSize: 500 }),
|
getPermissionListApi({ page: 1, pageSize: 500 }),
|
||||||
getUserGroupsApi(row.id),
|
getUserGroupsApi(row.id),
|
||||||
getUserPermissionsDirectApi(row.id),
|
getUserPermissionsDirectApi(row.id),
|
||||||
|
getUserRolesApi(row.id),
|
||||||
]);
|
]);
|
||||||
|
console.log('userGroups', userGroups);
|
||||||
allGroups.value = groupsRes.items || [];
|
allGroups.value = groupsRes.items || [];
|
||||||
allPermissions.value = permsRes.items || [];
|
allPermissions.value = permsRes.items || [];
|
||||||
selectedGroupIds.value = (userGroups || []).map((g) => String(g.id));
|
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 || [];
|
const directPerms = userDirectPerms || [];
|
||||||
selectedAllowPermIds.value = directPerms.filter((p) => p.grant_type === 1).map((p) => String(p.permission_id));
|
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));
|
selectedDenyPermIds.value = directPerms.filter((p) => p.grant_type === 2).map((p) => String(p.permission_id));
|
||||||
@ -113,13 +128,7 @@ async function handleConfirm() {
|
|||||||
]);
|
]);
|
||||||
message.success('权限分配成功');
|
message.success('权限分配成功');
|
||||||
modalOpen.value = false;
|
modalOpen.value = false;
|
||||||
// 刷新当前行缓存
|
await refreshUserRelationCache(currentAdminId.value);
|
||||||
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 || []);
|
|
||||||
} finally {
|
} finally {
|
||||||
modalLoading.value = false;
|
modalLoading.value = false;
|
||||||
}
|
}
|
||||||
@ -136,7 +145,7 @@ const gridOptions: VxeGridProps<UserInfo> = {
|
|||||||
{ field: 'userRoles', title: '权限组(角色)', minWidth: 200, slots: { default: 'userRolesSlot' } },
|
{ field: 'userRoles', title: '权限组(角色)', minWidth: 200, slots: { default: 'userRolesSlot' } },
|
||||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 120 },
|
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 120 },
|
||||||
],
|
],
|
||||||
height: '100%',
|
minHeight: 650,
|
||||||
pagerConfig: {},
|
pagerConfig: {},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
response: { total: 'total', result: 'data' },
|
response: { total: 'total', result: 'data' },
|
||||||
@ -144,6 +153,7 @@ const gridOptions: VxeGridProps<UserInfo> = {
|
|||||||
query: async () => {
|
query: async () => {
|
||||||
const result = await getAdminListApi();
|
const result = await getAdminListApi();
|
||||||
const list: UserInfo[] = Array.isArray(result) ? result : ((result as any)?.data ?? []);
|
const list: UserInfo[] = Array.isArray(result) ? result : ((result as any)?.data ?? []);
|
||||||
|
currentUsers.value = list;
|
||||||
void loadRowDataForList(list);
|
void loadRowDataForList(list);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
@ -153,6 +163,12 @@ const gridOptions: VxeGridProps<UserInfo> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
|
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
if (currentUsers.value.length > 0) {
|
||||||
|
void loadRowDataForList(currentUsers.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -140,7 +140,7 @@ const gridOptions: VxeGridProps<PermissionApi.UserGroup> = {
|
|||||||
{ field: 'remark', title: '备注', minWidth: 200 },
|
{ field: 'remark', title: '备注', minWidth: 200 },
|
||||||
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
{ fixed: 'right', slots: { default: 'actionSlot' }, title: '操作', width: 160 },
|
||||||
],
|
],
|
||||||
height: 'auto',
|
minHeight: 650,
|
||||||
pagerConfig: {},
|
pagerConfig: {},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
response: { result: 'items', total: 'total' },
|
response: { result: 'items', total: 'total' },
|
||||||
|
|||||||
@ -1,62 +1,279 @@
|
|||||||
<script setup lang="ts">
|
<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 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 type { UserInfo } from '#/model/admin.user';
|
||||||
import { getAdminListApi } from '#/api/core/admin.user';
|
import {
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
getAdminListApi,
|
||||||
import addUserModal from './addUser.vue';
|
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> = {
|
const gridOptions: VxeGridProps<UserInfo> = {
|
||||||
columns: [
|
columns: [
|
||||||
{ field: 'username', title: '用户名', },
|
{ field: 'id', title: 'ID', width: 60 },
|
||||||
// { field: 'phone', title: '手机号' },
|
{ field: 'username', title: '用户名', width: 120 },
|
||||||
{ field: 'role', title: '角色' },
|
{ field: 'real_name', title: '真实姓名', width: 110 },
|
||||||
{ field: 'group', title: '用户组' },
|
{ field: 'nickname', title: '昵称', width: 110 },
|
||||||
{ field: 'remark', title: '备注' },
|
{ 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: {},
|
pagerConfig: {},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
response: {
|
response: { result: 'data', total: 'total' },
|
||||||
total: 'total',
|
|
||||||
result: 'data',
|
|
||||||
},
|
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async () => {
|
query: async () => {
|
||||||
return await getAdminListApi();
|
return await getAdminListApi();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
rowConfig: { isHover: true },
|
||||||
rowConfig: {
|
|
||||||
isHover: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
|
const [Grid, GridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
const [addUserM, addUserApi] = useVbenModal({
|
|
||||||
connectedComponent: addUserModal,
|
function openCreate() {
|
||||||
onClosed: async () => {
|
editingId.value = undefined;
|
||||||
addUserApi.close();
|
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();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<addUserM class="w-[50%]" />
|
<FormModal>
|
||||||
<Card class="mb-5" title="用户操作">
|
<Form />
|
||||||
<Space>
|
</FormModal>
|
||||||
<Button @click="addAdmin">新增</Button>
|
|
||||||
<Button> 删除 </Button>
|
<Card class="mb-4">
|
||||||
</Space>
|
<template #title>管理员列表</template>
|
||||||
|
<template #extra>
|
||||||
|
<Button type="primary" @click="openCreate">新增管理员</Button>
|
||||||
|
</template>
|
||||||
</Card>
|
</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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -29,15 +29,7 @@ onMounted(async () => {
|
|||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#5ab1ef',
|
color: '#5ab1ef',
|
||||||
},
|
},
|
||||||
smooth: true,
|
name: '日活',
|
||||||
type: 'line',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
areaStyle: {},
|
|
||||||
data: data.value2,
|
|
||||||
itemStyle: {
|
|
||||||
color: '#019680',
|
|
||||||
},
|
|
||||||
smooth: true,
|
smooth: true,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
},
|
},
|
||||||
@ -51,14 +43,6 @@ onMounted(async () => {
|
|||||||
},
|
},
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
},
|
},
|
||||||
// xAxis: {
|
|
||||||
// axisTick: {
|
|
||||||
// show: false,
|
|
||||||
// },
|
|
||||||
// boundaryGap: false,
|
|
||||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
|
||||||
// type: 'category',
|
|
||||||
// },
|
|
||||||
xAxis: {
|
xAxis: {
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false,
|
show: false,
|
||||||
@ -79,7 +63,6 @@ onMounted(async () => {
|
|||||||
axisTick: {
|
axisTick: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
max: 5_000,
|
|
||||||
splitArea: {
|
splitArea: {
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { getstatisticsHeat } from '#/api/core/statistics';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EchartsUI,
|
EchartsUI,
|
||||||
@ -10,7 +11,8 @@ import {
|
|||||||
const chartRef = ref<EchartsUIType>();
|
const chartRef = ref<EchartsUIType>();
|
||||||
const { renderEcharts } = useEcharts(chartRef);
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
const data = await getstatisticsHeat({ AppId: 0 });
|
||||||
renderEcharts({
|
renderEcharts({
|
||||||
grid: {
|
grid: {
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@ -22,29 +24,24 @@ onMounted(() => {
|
|||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
barMaxWidth: 80,
|
barMaxWidth: 80,
|
||||||
// color: '#4f69fd',
|
data: data.value2,
|
||||||
data: [
|
name: '注册人数',
|
||||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
|
||||||
3200, 4800,
|
|
||||||
],
|
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tooltip: {
|
tooltip: {
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
// color: '#4f69fd',
|
|
||||||
width: 1,
|
width: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
data: data.key,
|
||||||
type: 'category',
|
type: 'category',
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
max: 8000,
|
|
||||||
splitNumber: 4,
|
splitNumber: 4,
|
||||||
type: 'value',
|
type: 'value',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,10 +3,6 @@ import { onMounted, ref } from 'vue';
|
|||||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||||
import type { TabOption } from '@vben/types';
|
import type { TabOption } from '@vben/types';
|
||||||
import { getstatisticsInfo } from '#/api/core/statistics';
|
import { getstatisticsInfo } from '#/api/core/statistics';
|
||||||
import { AccessControl } from '@vben/access';
|
|
||||||
import { useAccess } from '@vben/access';
|
|
||||||
|
|
||||||
const { hasAccessByRoles } = useAccess();
|
|
||||||
import {
|
import {
|
||||||
AnalysisChartCard,
|
AnalysisChartCard,
|
||||||
AnalysisChartsTabs,
|
AnalysisChartsTabs,
|
||||||
@ -77,11 +73,11 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
const chartTabs: TabOption[] = [
|
const chartTabs: TabOption[] = [
|
||||||
{
|
{
|
||||||
label: '热度趋势',
|
label: '日活',
|
||||||
value: 'trends',
|
value: 'trends',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '月访问量',
|
label: '注册人数',
|
||||||
value: 'visits',
|
value: 'visits',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -89,7 +85,6 @@ const chartTabs: TabOption[] = [
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<AccessControl :codes="['super', 'admin']" type="role">
|
|
||||||
<AnalysisOverview :items="overviewItems" />
|
<AnalysisOverview :items="overviewItems" />
|
||||||
|
|
||||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||||
@ -99,10 +94,9 @@ const chartTabs: TabOption[] = [
|
|||||||
<template #visits>
|
<template #visits>
|
||||||
<AnalyticsVisits />
|
<AnalyticsVisits />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</AnalysisChartsTabs>
|
</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="访问数量">
|
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||||
<AnalyticsVisitsData />
|
<AnalyticsVisitsData />
|
||||||
</AnalysisChartCard>
|
</AnalysisChartCard>
|
||||||
@ -112,7 +106,6 @@ const chartTabs: TabOption[] = [
|
|||||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||||
<AnalyticsVisitsSales />
|
<AnalyticsVisitsSales />
|
||||||
</AnalysisChartCard>
|
</AnalysisChartCard>
|
||||||
</div>
|
</div> -->
|
||||||
</AccessControl>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -72,7 +72,7 @@ const gridOptions: VxeGridProps<ActivityData> = {
|
|||||||
{ field: 'interval', title: '活动循环间隔(秒)从上次开始时间开始计算,0表示不循环', },
|
{ field: 'interval', title: '活动循环间隔(秒)从上次开始时间开始计算,0表示不循环', },
|
||||||
{ field: 'tag', title: '状态', slots: { default: 'tag' } },
|
{ field: 'tag', title: '状态', slots: { default: 'tag' } },
|
||||||
],
|
],
|
||||||
height: 'auto',
|
minHeight: '650px',
|
||||||
pagerConfig: {},
|
pagerConfig: {},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
response: {
|
response: {
|
||||||
@ -83,7 +83,6 @@ const gridOptions: VxeGridProps<ActivityData> = {
|
|||||||
query: async ({ page }, formValues) => {
|
query: async ({ page }, formValues) => {
|
||||||
let AppId = parseNumber(formValues.AppId);
|
let AppId = parseNumber(formValues.AppId);
|
||||||
let activityType = parseNumber(formValues.activityType);
|
let activityType = parseNumber(formValues.activityType);
|
||||||
console.log('query', formValues, page);
|
|
||||||
const response = await getActivityListApi({
|
const response = await getActivityListApi({
|
||||||
AppId: AppId,
|
AppId: AppId,
|
||||||
ServerId: formValues.ServerId,
|
ServerId: formValues.ServerId,
|
||||||
@ -125,6 +124,8 @@ const gridOptions: VxeGridProps<ActivityData> = {
|
|||||||
item.tag = '生效中';
|
item.tag = '生效中';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
return 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 dayjs from 'dayjs';
|
||||||
import { WorkbenchDetail } from '@vben/common-ui';
|
import { WorkbenchDetail } from '@vben/common-ui';
|
||||||
import UserHeader from './user-header.vue';
|
import UserHeader from './user-header.vue';
|
||||||
import { AccessControl } from '@vben/access';
|
import { AccessControl, useAccess } from '@vben/access';
|
||||||
import eventTable from './event-table.vue';
|
import eventTable from './event-table.vue';
|
||||||
import assetTable from './asset-table.vue';
|
import assetTable from './asset-table.vue';
|
||||||
import orderTable from './order-table.vue';
|
import orderTable from './order-table.vue';
|
||||||
@ -27,6 +27,14 @@ import orderTable from './order-table.vue';
|
|||||||
// 例如:url: /dashboard/workspace
|
// 例如:url: /dashboard/workspace
|
||||||
const projectItems: WorkbenchProjectItem[] = [];
|
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 chargeDisplay = computed(() => (Number(info.value?.Charge ?? 0)).toFixed(2));
|
||||||
|
|
||||||
const [BaseForm] = useVbenForm({
|
const [BaseForm] = useVbenForm({
|
||||||
@ -268,6 +276,8 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
data.value = modalApi.getData<Record<string, any>>();
|
data.value = modalApi.getData<Record<string, any>>();
|
||||||
|
const b = hasAccessByCodes(['AC3003']);
|
||||||
|
console.log('canUseGm', canUseGm.value, 'hasAccessByCodes AC3003', b);
|
||||||
try {
|
try {
|
||||||
const r = await getUserlogInfoApi({
|
const r = await getUserlogInfoApi({
|
||||||
Id: data.value.uid,
|
Id: data.value.uid,
|
||||||
@ -476,16 +486,18 @@ function formatActLog(type: number, content = ''): [string, string] {
|
|||||||
<template #energy>{{ info.Energy }} </template>
|
<template #energy>{{ info.Energy }} </template>
|
||||||
<template #diamond>{{ info.Diamond }}</template>
|
<template #diamond>{{ info.Diamond }}</template>
|
||||||
</UserHeader>
|
</UserHeader>
|
||||||
<AccessControl :codes="['super', 'admin']" type="role">
|
|
||||||
<div class="mt-5 flex">
|
<div class="mt-5 flex">
|
||||||
|
<AccessControl :codes="['AC3003']" type="code">
|
||||||
<Card class="card-box flex flex-col p-5 w-[50%]">
|
<Card class="card-box flex flex-col p-5 w-[50%]">
|
||||||
<BaseForm />
|
<BaseForm />
|
||||||
</Card>
|
</Card>
|
||||||
|
</AccessControl>
|
||||||
|
<AccessControl :codes="['AC3004']" type="code">
|
||||||
<Card class="card-box flex flex-col p-5 w-[45%] ml-5">
|
<Card class="card-box flex flex-col p-5 w-[45%] ml-5">
|
||||||
<BanForm />
|
<BanForm />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
|
</div>
|
||||||
<LoginHeatmap :app-id="data?.AppId" :uid="data?.uid" title="登录热力图" class="mt-5" />
|
<LoginHeatmap :app-id="data?.AppId" :uid="data?.uid" title="登录热力图" class="mt-5" />
|
||||||
<div class="mt-5 flex flex-col lg:flex-row">
|
<div class="mt-5 flex flex-col lg:flex-row">
|
||||||
<div class="mr-4 w-full lg:w-3/5">
|
<div class="mr-4 w-full lg:w-3/5">
|
||||||
|
|||||||
@ -15,6 +15,11 @@ interface UserInfo extends BasicUserInfo {
|
|||||||
* accessToken
|
* accessToken
|
||||||
*/
|
*/
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户有效单点权限码列表(由后端 /user/info 返回)
|
||||||
|
*/
|
||||||
|
permissions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { UserInfo };
|
export type { UserInfo };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user