This commit is contained in:
ZhuJW
2026-04-22 13:40:01 +08:00
commit d6bf4684d2
1146 changed files with 96233 additions and 0 deletions

3
apps/backend-mock/.env Normal file
View File

@@ -0,0 +1,3 @@
PORT=5320
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret

View File

@@ -0,0 +1,15 @@
# @vben/backend-mock
## Description
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
## Running the app
```bash
# development
$ pnpm run start
# production mode
$ pnpm run build
```

View File

@@ -0,0 +1,14 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const codes =
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
return useResponseSuccess(codes);
});

View File

@@ -0,0 +1,36 @@
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);
if (!password || !username) {
setResponseStatus(event, 400);
return useResponseError(
'BadRequestException',
'Username and password are required',
);
}
const findUser = MOCK_USERS.find(
(item) => item.username === username && item.password === password,
);
if (!findUser) {
clearRefreshTokenCookie(event);
return forbiddenResponse(event, 'Username or password is incorrect.');
}
const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({
...findUser,
accessToken,
});
});

View File

@@ -0,0 +1,15 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return useResponseSuccess('');
}
clearRefreshTokenCookie(event);
return useResponseSuccess('');
});

View File

@@ -0,0 +1,33 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return forbiddenResponse(event);
}
clearRefreshTokenCookie(event);
const userinfo = verifyRefreshToken(refreshToken);
if (!userinfo) {
return forbiddenResponse(event);
}
const findUser = MOCK_USERS.find(
(item) => item.username === userinfo.username,
);
if (!findUser) {
return forbiddenResponse(event);
}
const accessToken = generateAccessToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return accessToken;
});

View File

@@ -0,0 +1,13 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const menus =
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
return useResponseSuccess(menus);
});

View File

@@ -0,0 +1,5 @@
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));
return useResponseError(`${status}`);
});

View File

@@ -0,0 +1,73 @@
import { faker } from '@faker-js/faker';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem = {
id: faker.string.uuid(),
imageUrl: faker.image.avatar(),
imageUrl2: faker.image.avatar(),
open: faker.datatype.boolean(),
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
productName: faker.commerce.productName(),
price: faker.commerce.price(),
currency: faker.finance.currencyCode(),
quantity: faker.number.int({ min: 1, max: 100 }),
available: faker.datatype.boolean(),
category: faker.commerce.department(),
releaseDate: faker.date.past(),
rating: faker.number.float({ min: 1, max: 5 }),
description: faker.commerce.productDescription(),
weight: faker.number.float({ min: 0.1, max: 10 }),
color: faker.color.human(),
inProduction: faker.datatype.boolean(),
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
const listData = structuredClone(mockData);
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
listData.sort((a, b) => {
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
} else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
}
} else {
if (sortBy === 'price') {
return (
Number.parseFloat(b[sortBy as string]) -
Number.parseFloat(a[sortBy as string])
);
} else {
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
}
}
});
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});

View File

@@ -0,0 +1 @@
export default defineEventHandler(() => 'Test get handler');

View File

@@ -0,0 +1 @@
export default defineEventHandler(() => 'Test post handler');

View File

@@ -0,0 +1,10 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(userinfo);
});

View File

@@ -0,0 +1,7 @@
import type { NitroErrorHandler } from 'nitropack';
const errorHandler: NitroErrorHandler = function (error, event) {
event.node.res.end(`[Error Handler] ${error.stack}`);
};
export default errorHandler;

View File

@@ -0,0 +1,7 @@
export default defineEventHandler((event) => {
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';
return 'OK';
}
});

View File

@@ -0,0 +1,19 @@
import errorHandler from './error';
process.env.COMPATIBILITY_DATE = new Date().toISOString();
export default defineNitroConfig({
devErrorHandler: errorHandler,
errorHandler: '~/error',
routeRules: {
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',
},
},
},
});

View File

@@ -0,0 +1,21 @@
{
"name": "@vben/backend-mock",
"version": "0.0.1",
"description": "",
"private": true,
"license": "MIT",
"author": "",
"scripts": {
"build": "nitro build",
"start": "nitro dev"
},
"dependencies": {
"@faker-js/faker": "catalog:",
"jsonwebtoken": "catalog:",
"nitropack": "catalog:"
},
"devDependencies": {
"@types/jsonwebtoken": "catalog:",
"h3": "catalog:"
}
}

View File

@@ -0,0 +1,12 @@
export default defineEventHandler(() => {
return `
<h1>Hello Vben Admin</h1>
<h2>Mock service is starting</h2>
<ul>
<li><a href="/api/user">/api/user/info</a></li>
<li><a href="/api/menu">/api/menu/all</a></li>
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
<li><a href="/api/auth/login">/api/auth/login</a></li>
</ul>
`;
});

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "./.nitro/types/tsconfig.json"
}

View File

@@ -0,0 +1,26 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,
sameSite: 'none',
secure: true,
});
}
export function setRefreshTokenCookie(
event: H3Event<EventHandlerRequest>,
refreshToken: string,
) {
setCookie(event, 'jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'none',
secure: true,
});
}
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
const refreshToken = getCookie(event, 'jwt');
return refreshToken;
}

View File

@@ -0,0 +1,59 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
export interface UserPayload extends UserInfo {
iat: number;
exp: number;
}
export function generateAccessToken(user: UserInfo) {
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
}
export function generateRefreshToken(user: UserInfo) {
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
expiresIn: '30d',
});
}
export function verifyAccessToken(
event: H3Event<EventHandlerRequest>,
): null | Omit<UserInfo, 'password'> {
const authHeader = getHeader(event, 'Authorization');
if (!authHeader?.startsWith('Bearer')) {
return null;
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
export function verifyRefreshToken(
token: string,
): null | Omit<UserInfo, 'password'> {
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}

View File

@@ -0,0 +1,189 @@
export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
homePath?: string;
}
export const MOCK_USERS: UserInfo[] = [
{
id: 0,
password: '123456',
realName: 'Vben',
roles: ['super'],
username: 'vben',
},
{
id: 1,
password: '123456',
realName: 'Admin',
roles: ['admin'],
username: 'admin',
homePath: '/workspace',
},
{
id: 2,
password: '123456',
realName: 'Jack',
roles: ['user'],
username: 'jack',
homePath: '/analytics',
},
];
export const MOCK_CODES = [
// super
{
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
username: 'vben',
},
{
// admin
codes: ['AC_100010', 'AC_100020', 'AC_100030'],
username: 'admin',
},
{
// user
codes: ['AC_1000001', 'AC_1000002'],
username: 'jack',
},
];
const dashboardMenus = [
{
component: 'BasicLayout',
meta: {
order: -1,
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/',
redirect: '/analytics',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
title: 'page.dashboard.analytics',
},
},
{
name: 'Workspace',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
title: 'page.dashboard.workspace',
},
},
],
},
];
const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
const roleWithMenus = {
admin: {
component: '/demos/access/admin-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.adminVisible',
},
name: 'AccessAdminVisibleDemo',
path: '/demos/access/admin-visible',
},
super: {
component: '/demos/access/super-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.superVisible',
},
name: 'AccessSuperVisibleDemo',
path: '/demos/access/super-visible',
},
user: {
component: '/demos/access/user-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.userVisible',
},
name: 'AccessUserVisibleDemo',
path: '/demos/access/user-visible',
},
};
return [
{
component: 'BasicLayout',
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: 'demos.title',
},
name: 'Demos',
path: '/demos',
redirect: '/demos/access',
children: [
{
name: 'AccessDemos',
path: '/demosaccess',
meta: {
icon: 'mdi:cloud-key-outline',
title: 'demos.access.backendPermissions',
},
redirect: '/demos/access/page-control',
children: [
{
name: 'AccessPageControlDemo',
path: '/demos/access/page-control',
component: '/demos/access/index',
meta: {
icon: 'mdi:page-previous-outline',
title: 'demos.access.pageAccess',
},
},
{
name: 'AccessButtonControlDemo',
path: '/demos/access/button-control',
component: '/demos/access/button-control',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.buttonControl',
},
},
{
name: 'AccessMenuVisible403Demo',
path: '/demos/access/menu-visible-403',
component: '/demos/access/menu-visible-403',
meta: {
authority: ['no-body'],
icon: 'mdi:button-cursor',
menuVisibleWithForbidden: true,
title: 'demos.access.menuVisible403',
},
},
roleWithMenus[role],
],
},
],
},
];
};
export const MOCK_MENUS = [
{
menus: [...dashboardMenus, ...createDemosMenus('super')],
username: 'vben',
},
{
menus: [...dashboardMenus, ...createDemosMenus('admin')],
username: 'admin',
},
{
menus: [...dashboardMenus, ...createDemosMenus('user')],
username: 'jack',
},
];

View File

@@ -0,0 +1,68 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,
data,
error: null,
message: 'ok',
};
}
export function usePageResponseSuccess<T = any>(
page: number | string,
pageSize: number | string,
list: T[],
{ message = 'ok' } = {},
) {
const pageData = pagination(
Number.parseInt(`${page}`),
Number.parseInt(`${pageSize}`),
list,
);
return {
...useResponseSuccess({
items: pageData,
total: list.length,
}),
message,
};
}
export function useResponseError(message: string, error: any = null) {
return {
code: -1,
data: null,
error,
message,
};
}
export function forbiddenResponse(
event: H3Event<EventHandlerRequest>,
message = 'Forbidden Exception',
) {
setResponseStatus(event, 403);
return useResponseError(message, message);
}
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401);
return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function pagination<T = any>(
pageNo: number,
pageSize: number,
array: T[],
): T[] {
const offset = (pageNo - 1) * Number(pageSize);
return offset + Number(pageSize) >= array.length
? array.slice(offset)
: array.slice(offset, offset + Number(pageSize));
}

5
apps/web-ele/.env Normal file
View File

@@ -0,0 +1,5 @@
# 应用标题
VITE_APP_TITLE=FST Data Factory
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=FST-Data-Factory

View File

@@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api
VITE_VISUALIZER=true

View File

@@ -0,0 +1,16 @@
# 端口号
VITE_PORT=5777
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=/api
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=false
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true

View File

@@ -0,0 +1,23 @@
VITE_BASE=/
# 接口地址
# VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
# VITE_GLOB_API_URL=http://124.223.108.9:5000/api
# VITE_GLOB_API_URL=http://127.0.0.1:5000/apip
# VITE_GLOB_API_URL=http://10.0.220.217:5222/api
# VITE_GLOB_API_URL=http://10.204.22.142:5222/api
VITE_GLOB_API_URL=http://115.190.59.169:5000/api
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=none
# 是否开启 PWA
VITE_PWA=false
# vue-router 的模式
VITE_ROUTER_HISTORY=hash
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true

41
apps/web-ele/components.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

35
apps/web-ele/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="description" content="A Modern Back-end Management System" />
<meta name="keywords" content="Vben Admin Vue3 Vite" />
<meta name="author" content="Vben" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title>
<link rel="icon" href="/image/logo.png" />
<script>
// 生产环境下注入百度统计
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
var _hmt = _hmt || [];
(function () {
var hm = document.createElement('script');
hm.src =
'https://hm.baidu.com/hm.js?97352b16ed2df8c3860cf5a1a65fb4dd';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

56
apps/web-ele/package.json Normal file
View File

@@ -0,0 +1,56 @@
{
"name": "@vben/web-ele",
"version": "5.5.2",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-ele"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"dayjs": "catalog:",
"element-plus": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
},
"devDependencies": {
"unplugin-auto-import": "^19.3.0",
"unplugin-element-plus": "catalog:",
"unplugin-vue-components": "^28.8.0"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M10.589 12.5H15q.213 0 .356-.144t.144-.357t-.144-.356T15 11.5h-4.411l1.765-1.766q.14-.133.14-.34t-.14-.348t-.347-.14q-.208 0-.341.14l-2.389 2.389q-.242.242-.242.565t.242.566l2.389 2.388q.14.14.344.13q.204-.009.344-.15t.14-.347t-.14-.34zm1.414 8.5q-1.866 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709"/></svg>

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -0,0 +1,234 @@
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
ElButton,
ElCheckbox,
ElCheckboxButton,
ElCheckboxGroup,
ElDatePicker,
ElDivider,
ElInput,
ElInputNumber,
ElNotification,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElSelectV2,
ElSpace,
ElSwitch,
ElTimePicker,
ElTreeSelect,
ElUpload,
} from 'element-plus';
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'RadioGroup'
| 'Select'
| 'Space'
| 'Switch'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: ElSelectV2,
loadingSlot: 'loading',
visibleEvent: 'onVisibleChange',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: ElTreeSelect,
props: { label: 'label', children: 'children' },
nodeKey: 'value',
loadingSlot: 'loading',
optionsPropName: 'data',
visibleEvent: 'onVisibleChange',
},
slots,
);
},
Checkbox: ElCheckbox,
CheckboxGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options, isButton } = attrs;
if (Array.isArray(options)) {
defaultSlot = () =>
options.map((option) =>
h(isButton ? ElCheckboxButton : ElCheckbox, option),
);
}
}
return h(
ElCheckboxGroup,
{ ...props, ...attrs },
{ ...slots, default: defaultSlot },
);
},
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
},
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
},
Divider: ElDivider,
IconPicker: (props, { attrs, slots }) => {
return h(
IconPicker,
{
iconSlot: 'append',
modelValueProp: 'model-value',
inputComponent: ElInput,
...props,
...attrs,
},
slots,
);
},
Input: withDefaultPlaceholder(ElInput, 'input'),
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
RadioGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options } = attrs;
if (Array.isArray(options)) {
defaultSlot = () =>
options.map((option) =>
h(attrs.isButton ? ElRadioButton : ElRadio, option),
);
}
}
return h(
ElRadioGroup,
{ ...props, ...attrs },
{ ...slots, default: defaultSlot },
);
},
Select: (props, { attrs, slots }) => {
return h(ElSelectV2, { ...props, attrs }, slots);
},
Space: ElSpace,
Switch: ElSwitch,
TimePicker: (props, { attrs, slots }) => {
const { name, id, isRange } = props;
const extraProps: Recordable<any> = {};
if (isRange) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElTimePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
DatePicker: (props, { attrs, slots }) => {
const { name, id, type } = props;
const extraProps: Recordable<any> = {};
if (type && type.includes('range')) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElDatePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
Upload: ElUpload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
ElNotification({
title,
message: content,
position: 'bottom-right',
duration: 0,
type: 'success',
});
},
});
}
export { initComponentAdapter };

View File

@@ -0,0 +1,39 @@
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
},
},
defineRules: {
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<ComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

@@ -0,0 +1,197 @@
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { ElButton, ElImage } from 'element-plus';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
minHeight: 180,
formConfig: {
// 全局禁用vxe-table的表单配置使用formOptions
enabled: false,
},
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small',
},
button: {
mode: 'text', // 默认按钮样式
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
const src = row[column.field];
return h(ElImage, { src, previewSrcList: [src] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
ElButton,
{ size: 'small', link: true },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// 在您的全局配置中修改 CellText 渲染器
vxeUI.renderer.add('CellText', {
renderTableDefault(_, { row, column }) {
return h(
'div',
{ class: 'gap-0' },
(column.cellRender.props?.options || []).map((opt) => {
// 1. 处理 disabled 状态
const isDisabled =
typeof opt.disabled === 'function'
? row
? opt.disabled({ row })
: false
: !!opt.disabled;
// 2. 根据状态确定颜色
let statusClass = '';
if (isDisabled) {
// 禁用状态使用灰色系
statusClass =
'bg-gray-300 text-gray-500 cursor-not-allowed opacity-75';
} else {
// 根据 status 属性应用不同颜色
switch (opt.status) {
case 'primary':
statusClass = 'bg-blue-500 hover:bg-blue-600';
break;
case 'success':
statusClass = 'bg-green-500 hover:bg-green-600';
break;
case 'warning':
statusClass = 'bg-yellow-500 hover:bg-yellow-600';
break;
case 'danger':
statusClass = 'bg-red-500 hover:bg-red-600';
break;
case 'info':
statusClass = 'bg-gray-500 hover:bg-gray-600';
break;
default:
statusClass = 'bg-blue-500 hover:bg-blue-600';
}
}
return h(
'button',
{
class: `px-2 py-0.5 ${statusClass} text-white rounded text-sm transition-colors duration-200`,
onClick: () => {
if (!isDisabled) {
column.cellRender.props?.onClick?.(opt, row);
}
},
disabled: isDisabled,
},
opt.content,
);
}),
);
},
});
// 注册全局的 CellText 渲染器
vxeUI.renderer.add('CellStatus', {
renderTableDefault(_, { row, column }) {
// ✅ 关键修改:判断 options 是否为函数,如果是则执行它
const options =
typeof column.cellRender.props?.options === 'function'
? column.cellRender.props.options({ row, column }) // 执行函数,传入参数
: column.cellRender.props?.options || [];
return h(
'div',
{ class: 'gap-0' },
options.map((opt) => {
// 1. 处理 disabled 状态
const isDisabled =
typeof opt.disabled === 'function'
? row
? opt.disabled({ row })
: false
: !!opt.disabled;
// 2. 根据 status 属性应用不同颜色
let statusClass = '';
if (isDisabled) {
statusClass =
'bg-gray-300 text-gray-500 cursor-not-allowed opacity-75';
} else {
switch (opt.status) {
case 'primary':
statusClass = 'bg-blue-500 hover:bg-blue-600';
break;
case 'success':
statusClass = 'bg-green-500 hover:bg-green-600';
break;
case 'warning':
statusClass = 'bg-yellow-500 hover:bg-yellow-600';
break;
case 'danger':
statusClass = 'bg-red-500 hover:bg-red-600';
break;
case 'info':
statusClass = 'bg-gray-500 hover:bg-gray-600';
break;
default:
statusClass = 'bg-blue-500 hover:bg-blue-600';
}
}
return h(
'button',
{
class: `px-2 py-0.5 ${statusClass} text-white rounded text-sm transition-colors duration-200`,
onClick: () => {
if (!isDisabled) {
opt.onClick?.(opt, row); // 调用你定义的 onClick
}
},
disabled: isDisabled,
},
opt.content,
);
}),
);
},
});
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';

View File

@@ -0,0 +1,51 @@
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken?: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}

View File

@@ -0,0 +1,74 @@
import { requestClient } from '#/api/request';
export async function getBagListApi(data: any) {
return requestClient.post('/factory/getbaglist', data);
}
export async function getRetestBagListApi(data: any) {
return requestClient.post('/factory/getretestbaglist', data);
}
export async function getLevel1TagApi() {
return requestClient.get('/factory/getlevel1');
}
export async function getStatusApi() {
return requestClient.get('/factory/getstatus');
}
export async function getInfoApi(data: any) {
return requestClient.get('/factory/getinfo', { params: data });
}
export async function getAllTagApi(data: any) {
return requestClient.get('/factory/getalltag', { params: data });
}
export async function selectNoApi(data: any) {
return requestClient.post('/factory/tag-invalid', data);
}
export async function insertAllTagApi(data: any) {
return requestClient.post('/factory/batch-bag-record', data);
}
export async function getBagDetailApi(data: any) {
return requestClient.post('/factory/bag-detail', data);
}
export async function getExtendVideoApi(data: any) {
return requestClient.post('/factory/bag-joined-detail', data);
}
export async function mergeExtendVideoApi(data: any) {
return requestClient.post('/factory/mergebags', data);
}
export async function restoreExtendVideoApi(data: any) {
return requestClient.post('/factory/restore-mergebags', data);
}
export async function getBagTotalApi() {
return requestClient.get('/factory/bag-total');
}
export async function getBagEchartsApi(data: any) {
return requestClient.post('/factory/echarts', data);
}
export async function getUpdateBagApi(data: any) {
return requestClient.get('/factory/updatedinfo', { params: data });
}
export async function getBagTotalOfToBeCheckedApi() {
return requestClient.get('/factory/bag-total-tobe-checked');
}
// export async function getExportRetestBagListApi(data: any) {
// return requestClient.post('/factory/exportretestbaglist', data);
// }
export async function getExportRetestBagListApi(data: any) {
return requestClient.post('/factory/exportretestbaglist', data, { responseType: 'blob' });
}

View File

@@ -0,0 +1,3 @@
export * from './auth';
export * from './menu';
export * from './user';

View File

@@ -0,0 +1,25 @@
import { requestClient } from '#/api/request';
export async function getFstTreeTagApi() {
return requestClient.get('/label/fst-tree');
}
export async function getCreateLevelOneApi(data: any) {
return requestClient.post('/label/create-levelone', data);
}
export async function getUpdateAnnotationApi(data: any) {
return requestClient.post('/label/update-fst-annotation', data);
}
export async function getAddSubLabelApi(data: any) {
return requestClient.post('/label/add-fst', data);
}
export async function getSyncFstApi(data: any) {
return requestClient.post('/label/sync-fst', data);
}

View File

@@ -0,0 +1,10 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View File

@@ -0,0 +1,21 @@
import { requestClient } from '#/api/request';
export async function insertQaStutasApi(data: any) {
return requestClient.post('/inspect/insert-qa-status', data);
}
export async function getFinishListApi(data: any) {
return requestClient.post('/inspect/get-finish-list', data);
}
export async function insertDbIdsApi(data: any) {
return requestClient.post('/inspect/insert-db-ids', data);
}
export async function insertRootDbApi(data: any) {
return requestClient.post('/inspect/insert-rootdb', data);
}

View File

@@ -0,0 +1,19 @@
import { requestClient } from '#/api/request';
export async function getMenuListApi() {
return requestClient.get('/remote/fstmenu');
}
export async function getRootBagListApi(data: any) {
return requestClient.post('/remote/remote-baglist', data);
}
export async function getSubFstApi(data: any) {
return requestClient.get('/remote/sub-fst', { params: data });
}
export async function getFstByTypeApi(data: any) {
return requestClient.get('/remote/all-fst', { params: data });
}

View File

@@ -0,0 +1,26 @@
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.get<any>('/auth/userinfo');
}
export async function getUsersApi() {
return requestClient.get('/auth/getuserlist');
}
export async function getRolesApi() {
return requestClient.get('/auth/getroles');
}
export async function updateUserApi(data: any) {
return requestClient.post('/auth/updateuser', data);
}
export async function createUserApi(data: any) {
return requestClient.post('/auth/createuser', data);
}

View File

@@ -0,0 +1,38 @@
import { requestClient } from '#/api/request';
export async function insertCsvToDbApi(data: any) {
return requestClient.post('/vlm/insert-csv', data, {
headers: {
'Content-Type': 'multipart/form-data', // 直接在此处设置 headers 对象
}
});
}
export async function getModelsApi() {
return requestClient.get('/vlm/get-models');
}
export async function getDatasetsApi() {
return requestClient.get('/vlm/get-datasets');
}
export async function getRearchListApi(data: any) {
return requestClient.post('/vlm/get-search', data);
}
export async function getAllTagsApi() {
return requestClient.get('/vlm/get-alltags');
}
export async function InsertVlmFilterApi(data: any) {
return requestClient.post('/vlm/insert-vlm-filter', data);
}
export async function getVlmFilterListApi(data: any) {
return requestClient.post('/vlm/get-vlm-filter-list', data);
}
export async function sendFilterDataApi(data: any) {
return requestClient.post('/vlm/send-loacldb', data);
}

View File

@@ -0,0 +1 @@
export * from './core';

View File

@@ -0,0 +1,137 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { ElMessage } from 'element-plus';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) {
const client = new RequestClient({
baseURL,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// response数据解构
// client.addResponseInterceptor<HttpResponse>({
// fulfilled: (response) => {
// const { data: responseData, status } = response;
// const { code, data } = responseData;
// if (status >= 200 && status < 400 && code === 0) {
// return data;
// }
// console.log(8888,data,response);
// throw Object.assign({}, response, { response });
// },
// });
// 修改后的响应拦截器(只解构第一层)
client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => {
// 第一层解构:仅提取 status 和 data
const { status, data: responseData } = response;
// 直接操作 responseData 避免二次解构
// if (status >= 200 && status < 400 && responseData.code === 0) {
// return responseData.data; // 返回业务数据
// }
return responseData; // 返回业务数据
// 错误处理(保留完整响应结构)
const errorPayload = {
...response, // 保留原始响应
customMessage: "业务逻辑错误",
responseData // 挂载业务数据到错误对象
};
throw errorPayload;
}
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示
ElMessage.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

17
apps/web-ele/src/app.vue Normal file
View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { useElementPlusDesignTokens } from '@vben/hooks';
import { ElConfigProvider } from 'element-plus';
import { elementLocale } from '#/locales';
defineOptions({ name: 'App' });
useElementPlusDesignTokens();
</script>
<template>
<ElConfigProvider :locale="elementLocale">
<RouterView />
</ElConfigProvider>
</template>

View File

@@ -0,0 +1,65 @@
import { createApp, markRaw, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/ele';
import { useTitle } from '@vueuse/core';
import { ElLoading } from 'element-plus';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import App from './app.vue';
import { router } from './router';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import ElementPlus from 'element-plus'
import { initDynamicRoutes } from '#/router/routes/modules/rootdb';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
const app = createApp(App);
// 全局注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
// for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
// app.component(key, markRaw(component));
// }
// 注册Element Plus提供的v-loading指令
app.directive('loading', ElLoading.directive);
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 配置路由及路由守卫
app.use(router);
app.use(ElementPlus)
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
await initDynamicRoutes();
app.mount('#app');
}
export { bootstrap };

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const appName = computed(() => preferences.app.name);
// const logo = computed(() => preferences.logo.source);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="'/image/logo.png'"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
:toolbarList=[]
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
</template>

View File

@@ -0,0 +1,157 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([
{
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
message: '描述信息描述信息描述信息',
title: '收到了 14 份新周报',
},
{
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
message: '描述信息描述信息描述信息',
title: '朱偏右 回复了你',
},
{
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
message: '描述信息描述信息描述信息',
title: '曲丽丽 评论了你',
},
{
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '代办提醒',
},
]);
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => [
{
handler: () => {
openWindow(VBEN_DOC_URL, {
target: '_blank',
});
},
icon: BookOpenText,
text: $t('ui.widgets.document'),
},
{
handler: () => {
openWindow(VBEN_GITHUB_URL, {
target: '_blank',
});
},
icon: MdiGithub,
text: 'GitHub',
},
{
handler: () => {
openWindow(`${VBEN_GITHUB_URL}/issues`, {
target: '_blank',
});
},
icon: CircleHelp,
text: $t('ui.widgets.qa'),
},
]);
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
async function handleLogout() {
await authStore.logout(false);
}
function handleNoticeClear() {
notifications.value = [];
}
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
watch(
() => preferences.app.watermark,
async (enable) => {
if (enable) {
await updateWatermark({
content: `${userStore.userInfo?.username}`,
});
} else {
destroyWatermark();
}
},
{
immediate: true,
},
);
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown>
<UserDropdown
:avatar
:menus
:text="userStore.userInfo?.realName"
description=""
tag-text="Pro"
@logout="handleLogout"
/>
</template>
<template #notification>
<Notification
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@make-all="handleMakeAll"
/>
</template>
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout>
</template>

View File

@@ -0,0 +1,6 @@
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };

View File

@@ -0,0 +1,3 @@
# locale
每个app使用的国际化可能不同这里用于扩展国际化的功能例如扩展 dayjs、antd组件库的多语言切换以及app本身的国际化文件。

View File

@@ -0,0 +1,100 @@
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import type { Language } from 'element-plus/es/locale';
import type { App } from 'vue';
import { ref } from 'vue';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import dayjs from 'dayjs';
import enLocale from 'element-plus/es/locale/lang/en';
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
const elementLocale = ref<Language>(defaultLocale);
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
* 加载第三方组件库的语言包
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadElementLocale(lang), loadDayjsLocale(lang)]);
}
/**
* 加载dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* 加载element-plus的语言包
* @param lang
*/
async function loadElementLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'en-US': {
elementLocale.value = enLocale;
break;
}
case 'zh-CN': {
elementLocale.value = defaultLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, elementLocale, setupI18n };

View File

@@ -0,0 +1,13 @@
{
"title": "Demos",
"elementPlus": "Element Plus",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}

View File

@@ -0,0 +1,14 @@
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}

View File

@@ -0,0 +1,13 @@
{
"title": "演示",
"elementPlus": "Element Plus",
"form": "表单演示",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}

View File

@@ -0,0 +1,25 @@
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
},
"dashboard": {
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
},
"datamanage":{
"title":"数据管理",
"display":"数据概览",
"datalabel":"数据标注"
},
"usercenter":{
"title":"用户中心",
"usermanage":"用户管理",
"logging":"日志记录",
"tagmanage":"标签管理"
}
}

31
apps/web-ele/src/main.ts Normal file
View File

@@ -0,0 +1,31 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();

View File

@@ -0,0 +1,26 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
* !!! 更改配置后请清空缓存,否则可能不生效
*/
export const overridesPreferences = defineOverridesPreferences({
app: {
name: 'FST Data Factory',
accessMode: 'frontend',
loginExpiredMode: 'page',
},
sidebar: {
width: 220,
},
theme: {
builtinType: 'deep-green',
// colorPrimary: 'hsl(181 84% 32%)',
colorPrimary: 'hsl(245 82% 67%)',
mode: 'light',
},
transition: {
name: 'fade',
},
});

View File

@@ -0,0 +1,42 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { ElMessage } from 'element-plus';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
ElMessage({
duration: 1500,
message: `${$t('common.loadingMenu')}...`,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };

View File

@@ -0,0 +1,139 @@
import type { Router } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// console.log(1111, to.meta.loaded);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
console.log(2222, to.path);
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH,
);
}
return true;
}
// accessToken 检查
console.log(11118, accessStore.accessToken);
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
// console.log(11118888, to.meta.ignoreAccess);
if (to.meta.ignoreAccess) {
return true;
}
// console.log(11118888, to.fullPath);
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === DEFAULT_HOME_PATH
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
// console.log(108888, accessStore.isAccessChecked);
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const wws =await authStore.fetchUserInfo()
// console.log(3333,wws)
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
// console.log(2222,userInfo)
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示但是访问会被重定向到403
routes: accessRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH
? userInfo.homePath || DEFAULT_HOME_PATH
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };

View File

@@ -0,0 +1,37 @@
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };

View File

@@ -0,0 +1,88 @@
import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
{
meta: {
title: 'Root',
},
name: 'Root',
path: '/',
redirect: DEFAULT_HOME_PATH,
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: Login,
meta: {
title: $t('page.auth.login'),
},
},
// {
// name: 'CodeLogin',
// path: 'code-login',
// component: () => import('#/views/_core/authentication/code-login.vue'),
// meta: {
// title: $t('page.auth.codeLogin'),
// },
// },
// {
// name: 'QrCodeLogin',
// path: 'qrcode-login',
// component: () =>
// import('#/views/_core/authentication/qrcode-login.vue'),
// meta: {
// title: $t('page.auth.qrcodeLogin'),
// },
// },
// {
// name: 'ForgetPassword',
// path: 'forget-password',
// component: () =>
// import('#/views/_core/authentication/forget-password.vue'),
// meta: {
// title: $t('page.auth.forgetPassword'),
// },
// },
// {
// name: 'Register',
// path: 'register',
// component: () => import('#/views/_core/authentication/register.vue'),
// meta: {
// title: $t('page.auth.register'),
// },
// },
],
},
];
export { coreRoutes, fallbackNotFoundRoute };

View File

@@ -0,0 +1,37 @@
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表由基本路由、外部路由和404兜底路由组成
* 无需走权限验证(会一直显示在菜单中) */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
export { accessRoutes, coreRouteNames, routes };

View File

@@ -0,0 +1,45 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
// <el-icon><FolderOpened /></el-icon>
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'lucide:layout-dashboard',
// icon:FolderOpened,
order: -1,
title: $t('page.dashboard.title'),
hideInMenu: true
},
name: 'Dashboard',
path: '/',
children: [
{
name: 'Analytics',
path: '/analytics',
// component: () => import('#/views/dashboard/analytics/index.vue'),
component: () => import('#/views/datamanage/display/dataStatistics.vue'),
meta: {
affixTab: false,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
// authority:['user']
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/datamanage/display/dataStatistics.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;

View File

@@ -0,0 +1,65 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
// import { FolderOpened, DataAnalysis, EditPen, Filter, Position } from '@element-plus/icons-vue';
// import { markRaw } from 'vue';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
// icon: 'lucide:layout-dashboard',
icon: "ep:folder-opened",
order: -1,
title: $t('page.datamanage.title'),
},
name: 'Datamanage',
path: '/datamanage',
children: [
{
name: 'Display',
path: '/datamanage/display',
component: () =>
import('#/views/datamanage/display/dataStatistics.vue'),
meta: {
affixTab: false,
icon: "ep:data-analysis",
title: $t('page.datamanage.display'),
},
},
{
name: 'Datalabel',
path: '/datamanage/datalabel',
component: () => import('#/views/datamanage/datalabel/bag_table.vue'),
meta: {
// icon: markRaw(EditPen),
icon: "ep:edit-pen",
title: $t('page.datamanage.datalabel'),
},
},
{
name: 'Retestlabel',
path: '/datamanage/retestlabel',
component: () => import('#/views/datamanage/datalabel/retestlabel.vue'),
meta: {
icon: "ep:filter",
title: '复检数据',
authority: ['admin', 'check']
},
},
{
name: 'FinishProcess',
path: '/datamanage/finishprocess',
component: () => import('#/views/datamanage/datalabel/finishprocess.vue'),
meta: {
icon: "ep:position",
title: '推送结果',
authority: ['admin', 'check']
},
},
],
},
];
export default routes;

View File

@@ -0,0 +1,38 @@
// import type { RouteRecordRaw } from 'vue-router';
// import { BasicLayout } from '#/layouts';
// import { $t } from '#/locales';
// const routes: RouteRecordRaw[] = [
// {
// component: BasicLayout,
// meta: {
// icon: 'ic:baseline-view-in-ar',
// keepAlive: true,
// order: 1000,
// title: $t('demos.title'),
// },
// name: 'Demos',
// path: '/demos',
// children: [
// {
// meta: {
// title: $t('demos.elementPlus'),
// },
// name: 'NaiveDemos',
// path: '/demos/element',
// component: () => import('#/views/demos/element/index.vue'),
// },
// {
// meta: {
// title: $t('demos.form'),
// },
// name: 'BasicForm',
// path: '/demos/form',
// component: () => import('#/views/demos/form/basic.vue'),
// },
// ],
// },
// ];
// export default routes;

View File

@@ -0,0 +1,58 @@
import type { RouteRecordRaw } from 'vue-router';
import Detail from '#/views/detailpage/baginfo/index.vue';
import DetailUpdated from '#/views/detailpage/baginfo/indexupdated.vue';
import VlmDetail from '#/views/vlm/vlmsync/vlmbagdetail.vue'
const routes: RouteRecordRaw[] = [
{
component: Detail,
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: '详情页',
hideInMenu: true,
},
name: 'Detail',
path: '/detail/:id',
children: [
// {
// name: 'Display',
// path: '/datamanage/display',
// component: () => import('#/views/dashboard/analytics/index.vue'),
// meta: {
// affixTab: true,
// icon: 'lucide:area-chart',
// title: $t('page.datamanage.display'),
// },
// }
],
},
{
component: DetailUpdated,
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: '详情页',
hideInMenu: true,
authority: ['admin', 'check']
},
name: 'DetailUpdated',
path: '/detailupdated/:id',
},
{
component: VlmDetail,
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: '详情页',
hideInMenu: true,
authority: ['admin', 'check']
},
name: 'VlmDetail',
path: '/vlmdetail',
},
];
export default routes;

View File

@@ -0,0 +1,44 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
// import { CollectionTag, PriceTag, Wallet } from '@element-plus/icons-vue';
// import { markRaw } from 'vue';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: "ep:collection-tag",
keepAlive: true,
order: 100,
title: $t('FST标签管理'),
hideInMenu: false,
authority: ['admin']
},
name: 'Labelmanage',
path: '/labelmanage',
children: [
{
meta: {
title: $t('增改 Fst标签'),
icon: "ep:price-tag"
},
name: 'modifylabel',
path: '/labelmanage/modifylabel',
component: () => import('#/views/labelmanage/modifylabel/index.vue'),
},
{
meta: {
title: $t('展示RootDB Fst标签'),
icon: "ep:wallet"
},
name: 'showremotelabel',
path: '/labelmanage/showremotelabel',
component: () => import('#/views/labelmanage/showremotelabel/index.vue'),
}
],
},
];
export default routes;

View File

@@ -0,0 +1,137 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
import { getMenuListApi } from '#/api/core/rootdata';
import { Files, } from '@element-plus/icons-vue';
import { markRaw } from 'vue';
export interface BackendRoute {
path: string
name: string
title: string
componentConfig?: any
children?: BackendRoute[]
}
// 缓存相关常量
const CACHE_KEY = 'dynamic_routes_cache';
const CACHE_EXPIRE_TIME = 24 * 60 * 60 * 1000; // 缓存有效期24小时
// 获取缓存的路由数据
const getCachedRoutes = (): BackendRoute[] | null => {
try {
const cached = localStorage.getItem(CACHE_KEY);
if (!cached) return null;
const { data, timestamp } = JSON.parse(cached);
// 检查缓存是否过期
if (Date.now() - timestamp < CACHE_EXPIRE_TIME) {
return data;
}
// 缓存过期,清除缓存
localStorage.removeItem(CACHE_KEY);
return null;
} catch (error) {
console.error('获取路由缓存失败:', error);
return null;
}
};
// 缓存路由数据
const cacheRoutes = (routes: BackendRoute[]): void => {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({
data: routes,
timestamp: Date.now()
}));
} catch (error) {
console.error('缓存路由失败:', error);
}
};
// 清除路由缓存
export const clearRouteCache = (): void => {
localStorage.removeItem(CACHE_KEY);
};
const generateRoutes = (menuList: BackendRoute[]): RouteRecordRaw[] => {
return menuList.map(item => {
const routePath = `/processdata/${item.path}`;
return {
path: routePath,
name: item.name,
component: () =>
import(`#/views/datamanage/remotedata/index.vue`).then(m => m.default || m),
meta: {
title: item.title,
routeName: item.name,
uniqueData: item.componentConfig,
icon: "ep:wallet"
}
}
})
};
// 动态加载并添加路由(带缓存)
export const initDynamicRoutes = async (forceRefresh = false) => {
try {
let menuList: BackendRoute[];
// 如果不是强制刷新,先尝试从缓存获取
if (!forceRefresh) {
const cachedRoutes = getCachedRoutes();
if (cachedRoutes) {
console.log('使用缓存的路由数据');
menuList = cachedRoutes;
} else {
// 缓存不存在或过期,请求后端
console.log('缓存不存在,请求后端路由数据');
menuList = await getMenuListApi();
// 缓存新获取的路由数据
cacheRoutes(menuList);
}
} else {
// 强制刷新,直接请求后端
console.log('强制刷新,请求后端路由数据');
menuList = await getMenuListApi();
cacheRoutes(menuList);
}
const dynamicRoutes = generateRoutes(menuList);
// console.log('动态路由', dynamicRoutes);
// 找到父路由并添加子路由
const parentRoute = routes.find(route => route.name === 'ProcessData');
if (parentRoute && parentRoute.children) {
parentRoute.children = []; // 清空原有子路由
parentRoute.children.push(...dynamicRoutes);
}
return dynamicRoutes;
} catch (error) {
console.error('路由加载失败:', error);
return [];
}
};
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: "ep:files",
keepAlive: true,
order: 1000,
title: $t('FST数据浏览器'),
hideInMenu: false,
authority: ['admin']
},
name: 'ProcessData',
path: '/processdata',
children: [
// 动态路由将在这里被添加
]
},
];
export default routes;

View File

@@ -0,0 +1,51 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('page.usercenter.title'),
hideInMenu: false,
authority: ['admin'],
},
name: 'Usercenter',
path: '/usercenter',
children: [
{
meta: {
title: $t('page.usercenter.usermanage'),
authority: ['admin'],
},
name: 'Usermanage',
path: '/usercenter/Usermanage',
component: () => import('#/views/usercenter/usermanage.vue'),
},
{
meta: {
title: $t('page.usercenter.logging'),
hideInMenu: true,
},
name: 'Logging',
path: '/usercenter/logging',
component: () => import('#/views/demos/form/basic.vue'),
},
{
meta: {
title: $t('page.usercenter.tagmanage'),
hideInMenu: true,
},
name: 'Tagmanage',
path: '/usercenter/tagmanage',
component: () => import('#/views/demos/form/basic.vue'),
},
],
},
];
export default routes;

View File

@@ -0,0 +1,82 @@
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
} from '@vben/constants';
import { SvgAntdvLogoIcon } from '@vben/icons';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
badgeType: 'dot',
icon: VBEN_LOGO_URL,
order: 9999,
title: $t('demos.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('demos.vben.about'),
},
},
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
title: $t('demos.vben.document'),
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: VBEN_GITHUB_URL,
title: 'Github',
},
},
{
name: 'VbenNaive',
path: '/vben-admin/naive',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: 'logos:naiveui',
link: VBEN_NAIVE_PREVIEW_URL,
title: $t('demos.vben.naive-ui'),
},
},
{
name: 'VbenAntd',
path: '/vben-admin/antd',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: SvgAntdvLogoIcon,
link: VBEN_ANT_PREVIEW_URL,
title: $t('demos.vben.antdv'),
},
},
],
},
];
// export default routes;

View File

@@ -0,0 +1,54 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
// import { Reading, Tickets, Search, Finished } from '@element-plus/icons-vue';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
// icon: 'ic:baseline-view-in-ar',
icon: "ep:reading",
keepAlive: true,
order: 10,
title: $t('vlm数据管理'),
hideInMenu: false,
authority: ['admin', 'check']
},
name: 'VlmData',
path: '/vlmdata',
children: [
{
meta: {
title: $t('csv数据入库'),
icon: "ep:tickets"
},
name: 'InsertCsv',
path: '/vlmdata/insertcsv',
component: () => import('#/views/vlm/csvsync/index.vue'),
},
{
meta: {
title: $t('vlm 搜索'),
icon: "ep:search"
},
name: 'vlmSearch',
path: '/vlmdata/search',
component: () => import('#/views/vlm/vlmsync/search.vue'),
},
{
meta: {
title: $t('vlm 筛选展示'),
icon: "ep:finished"
},
name: 'vlmFilter',
path: '/vlmdata/filter',
component: () => import('#/views/vlm/vlmfilter/index.vue'),
}
],
},
];
export default routes;

View File

@@ -0,0 +1,169 @@
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { ElNotification } from 'element-plus';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param params 登录表单数据
*/
async function authLogin(
params: Recordable<any>,
onSuccess?: () => Promise<void> | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
console.log(1234599,params)
try {
loginLoading.value = true;
const { accessToken } = await loginApi(params);
// const accessToken=
// "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc1MjI2NDU0OCwianRpIjoiMzE4N2FiMWQtYzk1YS00MGNiLWI4ODEtM2M3ZWUyODJmMTA1IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6IjEiLCJuYmYiOjE3NTIyNjQ1NDgsImNzcmYiOiI2ZTcyZGE1MS1iNzFhLTRkZDktYWEwMC01OWI1ODRiZWQzYzMiLCJleHAiOjE3NTIyNzE3NDh9.3Pt7cWq_zIbTM3pM5_Y3wRVLZb2-tY-sH0EQUEJUUIQ"
// console.log(1234567,accessToken)
// 如果成功获取到 accessToken
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken);
// console.log(987)
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
}
if (userInfo?.realName) {
ElNotification({
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
title: $t('authentication.loginSuccess'),
type: 'success',
});
}
}
} finally {
loginLoading.value = false;
}
console.log(12345267,userInfo)
return {
userInfo,
};
}
async function logout(redirect: boolean = true) {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
// userInfo = await getUserInfoApi();
userInfo =await getUserInfoApi();
// userInfo={
// "roles": ["admin"],
// "realName": "admin",
// "homePath": "/analytics",
// // "avatar":'',
// // "userId":"",
// // "username":''
// }
// console.log(1234567)
userStore.setUserInfo(userInfo);
return userInfo;
}
function fetchUserInfoSync() {
let userInfo: null | UserInfo = null;
// 创建 Promise 包装异步操作
const promise = getUserInfoApi();
// 阻塞等待直到 Promise 完成
let resolved = false;
let result: any = null;
promise.then(res => {
resolved = true;
result = res;
}).catch(err => {
resolved = true;
result = err;
});
// 循环阻塞主线程(谨慎使用!)
while (!resolved) {
// 空循环,等待 Promise 状态变更
}
// 使用模拟数据(或实际 API 结果)
userInfo = {
roles: ["admin"],
realName: "admin",
homePath: "/analytics"
};
// console.log(1234567, userInfo);
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
};
});

View File

@@ -0,0 +1 @@
export * from './auth';

View File

@@ -0,0 +1,68 @@
// stores/vlmStore.ts
import { defineStore } from 'pinia';
// export const useVlmStore = defineStore('vlm', {
// state: () => ({
// vlmList: [], // 存储完整数据列表
// currentIndex: -1 // 存储当前点击项的下标
// }),
// actions: {
// // 新增:设置列表和下标
// setVlmData(data: { list: any[], currentIndex: number }) {
// this.vlmList = data.list;
// this.currentIndex = data.currentIndex;
// }
// },
// getters: {
// // 新增:根据下标获取当前项
// currentItem(): any {
// if (this.currentIndex >= 0 && this.currentIndex < this.vlmList.length) {
// return this.vlmList[this.currentIndex];
// }
// return null;
// }
// }
// });
export const useVlmStore = defineStore('vlm', {
state: () => ({
// 原有字段
vlmList: [],
currentIndex: -1,
// 新增:存储列表页搜索结果
searchResultList: [],
// 新增存储列表页滚动位置单位px
listScrollTop: 0,
// 新增:存储列表页的搜索参数(可选,确保重新加载时参数一致)
searchParams: {}
}),
actions: {
// 原有方法
setVlmData(data: { list: any[], currentIndex: number }) {
this.vlmList = data.list;
this.currentIndex = data.currentIndex;
},
// 新增:保存列表页搜索结果和参数
saveSearchResult(data: { result: any[], params: any }) {
this.searchResultList = data.result;
this.searchParams = data.params;
},
// 新增:保存列表页滚动位置
saveListScrollTop(scrollTop: number) {
this.listScrollTop = scrollTop;
},
// 新增:清空列表页缓存(如重新搜索时)
clearListCache() {
this.listScrollTop = 0;
}
},
getters: {
currentItem(): any {
if (this.currentIndex >= 0 && this.currentIndex < this.vlmList.length) {
return this.vlmList[this.currentIndex];
}
return null;
}
}
});

View File

@@ -0,0 +1,3 @@
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { About } from '@vben/common-ui';
defineOptions({ name: 'About' });
</script>
<template>
<About />
</template>

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.mobile'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
codeLength: CODE_LENGTH,
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
},
];
});
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console
console.log(values);
}
</script>
<template>
<AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"
/>
</template>

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'ForgetPassword' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: 'example@example.com',
},
fieldName: 'email',
label: $t('authentication.email'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
},
];
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
}
</script>
<template>
<AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@@ -0,0 +1,102 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import { computed, markRaw } from 'vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' });
const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: 'Super',
value: 'vben',
},
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'jack',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
// {
// component: 'VbenSelect',
// componentProps: {
// options: MOCK_USER_OPTIONS,
// placeholder: $t('authentication.selectAccount'),
// },
// fieldName: 'selectAccount',
// label: $t('authentication.selectAccount'),
// rules: z
// .string()
// .min(1, { message: $t('authentication.selectAccount') })
// .optional()
// .default('vben'),
// },
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
password: '123456',
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
// {
// component: markRaw(SliderCaptcha),
// fieldName: 'captcha',
// rules: z.boolean().refine((value) => value, {
// message: $t('authentication.verifyRequiredTip'),
// }),
// },
];
});
</script>
<template>
<AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading"
@submit="authStore.authLogin"
/>
</template>
function loginApi(arg0: { username: string; password: string; }) {
throw new Error('Function not implemented.');
}

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'QrCodeLogin' });
</script>
<template>
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
</template>

View File

@@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, h, ref } from 'vue';
import { AuthenticationRegister, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'Register' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
renderComponentContent() {
return {
strengthText: () => $t('authentication.passwordStrength'),
};
},
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.confirmPassword'),
},
dependencies: {
rules(values) {
const { password } = values;
return z
.string({ required_error: $t('authentication.passwordTip') })
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
});
},
triggerFields: ['password'],
},
fieldName: 'confirmPassword',
label: $t('authentication.confirmPassword'),
},
{
component: 'VbenCheckbox',
fieldName: 'agreePolicy',
renderComponentContent: () => ({
default: () =>
h('span', [
$t('authentication.agree'),
h(
'a',
{
class: 'vben-link ml-1 ',
href: '',
},
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
),
]),
}),
rules: z.boolean().refine((value) => !!value, {
message: $t('authentication.agreeTip'),
}),
},
];
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
}
</script>
<template>
<AuthenticationRegister
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback403Demo' });
</script>
<template>
<Fallback status="403" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback500Demo' });
</script>
<template>
<Fallback status="500" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback404Demo' });
</script>
<template>
<Fallback status="404" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'FallbackOfflineDemo' });
</script>
<template>
<Fallback status="offline" />
</template>

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2 %',
},
series: [
{
areaStyle: {},
data: [
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
111,
],
itemStyle: {
color: '#5ab1ef',
},
smooth: true,
type: 'line',
},
{
areaStyle: {},
data: [
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
],
itemStyle: {
color: '#019680',
},
smooth: true,
type: 'line',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
width: 1,
},
},
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,
},
boundaryGap: false,
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category',
},
yAxis: [
{
axisTick: {
show: false,
},
max: 80_000,
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value',
},
],
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -0,0 +1,84 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
legend: {
bottom: 0,
data: ['访问', '趋势'],
},
radar: {
indicator: [
{
name: '网页',
},
{
name: '移动端',
},
{
name: 'Ipad',
},
{
name: '客户端',
},
{
name: '第三方',
},
{
name: '其它',
},
],
radius: '60%',
splitNumber: 8,
},
series: [
{
areaStyle: {
opacity: 1,
shadowBlur: 0,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
},
data: [
{
itemStyle: {
color: '#b6a2de',
},
name: '访问',
value: [90, 50, 86, 40, 50, 20],
},
{
itemStyle: {
color: '#5ab1ef',
},
name: '趋势',
value: [70, 75, 70, 76, 20, 85],
},
],
itemStyle: {
// borderColor: '#fff',
borderRadius: 10,
borderWidth: 2,
},
symbolSize: 0,
type: 'radar',
},
],
tooltip: {},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -0,0 +1,48 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
series: [
{
animationDelay() {
return Math.random() * 400;
},
animationEasing: 'exponentialInOut',
animationType: 'scale',
center: ['50%', '50%'],
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: [
{ name: '外包', value: 500 },
{ name: '定制', value: 310 },
{ name: '技术支持', value: 274 },
{ name: '远程', value: 400 },
].sort((a, b) => {
return a.value - b.value;
}),
name: '商业占比',
radius: '80%',
roseType: 'radius',
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
legend: {
bottom: '2%',
left: 'center',
},
series: [
{
animationDelay() {
return Math.random() * 100;
},
animationEasing: 'exponentialInOut',
animationType: 'scale',
avoidLabelOverlap: false,
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: [
{ name: '搜索引擎', value: 1048 },
{ name: '直接访问', value: 735 },
{ name: '邮件营销', value: 580 },
{ name: '联盟广告', value: 484 },
],
emphasis: {
label: {
fontSize: '12',
fontWeight: 'bold',
show: true,
},
},
itemStyle: {
// borderColor: '#fff',
borderRadius: 10,
borderWidth: 2,
},
label: {
position: 'center',
show: false,
},
labelLine: {
show: false,
},
name: '访问来源',
radius: ['40%', '65%'],
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2 %',
},
series: [
{
barMaxWidth: 80,
// color: '#4f69fd',
data: [
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
3200, 4800,
],
type: 'bar',
},
],
tooltip: {
axisPointer: {
lineStyle: {
// color: '#4f69fd',
width: 1,
},
},
trigger: 'axis',
},
xAxis: {
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}`),
type: 'category',
},
yAxis: {
max: 8000,
splitNumber: 4,
type: 'value',
},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { TabOption } from '@vben/types';
import {
AnalysisChartCard,
AnalysisChartsTabs,
AnalysisOverview,
} from '@vben/common-ui';
import {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import AnalyticsTrends from './analytics-trends.vue';
import AnalyticsVisits from './analytics-visits.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue';
const overviewItems: AnalysisOverviewItem[] = [
{
icon: SvgCardIcon,
title: '用户量',
totalTitle: '总用户量',
totalValue: 120_000,
value: 2000,
},
{
icon: SvgCakeIcon,
title: '访问量',
totalTitle: '总访问量',
totalValue: 500_000,
value: 20_000,
},
{
icon: SvgDownloadIcon,
title: '下载量',
totalTitle: '总下载量',
totalValue: 120_000,
value: 8000,
},
{
icon: SvgBellIcon,
title: '使用量',
totalTitle: '总使用量',
totalValue: 50_000,
value: 5000,
},
];
const chartTabs: TabOption[] = [
{
label: '流量趋势',
value: 'trends',
},
{
label: '月访问量',
value: 'visits',
},
];
</script>
<template>
<div class="p-5">
<AnalysisOverview :items="overviewItems" />
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<AnalyticsTrends />
</template>
<template #visits>
<AnalyticsVisits />
</template>
</AnalysisChartsTabs>
<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>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSales />
</AnalysisChartCard>
</div>
</div>
</template>

View File

@@ -0,0 +1,266 @@
<script lang="ts" setup>
import type {
WorkbenchProjectItem,
WorkbenchQuickNavItem,
WorkbenchTodoItem,
WorkbenchTrendItem,
} from '@vben/common-ui';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import {
AnalysisChartCard,
WorkbenchHeader,
WorkbenchProject,
WorkbenchQuickNav,
WorkbenchTodo,
WorkbenchTrends,
} from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
const userStore = useUserStore();
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
// 例如url: /dashboard/workspace
const projectItems: WorkbenchProjectItem[] = [
{
color: '',
content: '不要等待机会,而要创造机会。',
date: '2021-04-01',
group: '开源组',
icon: 'carbon:logo-github',
title: 'Github',
url: 'https://github.com',
},
{
color: '#3fb27f',
content: '现在的你决定将来的你。',
date: '2021-04-01',
group: '算法组',
icon: 'ion:logo-vue',
title: 'Vue',
url: 'https://vuejs.org',
},
{
color: '#e18525',
content: '没有什么才能比努力更重要。',
date: '2021-04-01',
group: '上班摸鱼',
icon: 'ion:logo-html5',
title: 'Html5',
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
},
{
color: '#bf0c2c',
content: '热情和欲望可以突破一切难关。',
date: '2021-04-01',
group: 'UI',
icon: 'ion:logo-angular',
title: 'Angular',
url: 'https://angular.io',
},
{
color: '#00d8ff',
content: '健康的身体是实现目标的基石。',
date: '2021-04-01',
group: '技术牛',
icon: 'bx:bxl-react',
title: 'React',
url: 'https://reactjs.org',
},
{
color: '#EBD94E',
content: '路是走出来的,而不是空想出来的。',
date: '2021-04-01',
group: '架构组',
icon: 'ion:logo-javascript',
title: 'Js',
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
},
];
// 同样,这里的 url 也可以使用以 http 开头的外部链接
const quickNavItems: WorkbenchQuickNavItem[] = [
{
color: '#1fdaca',
icon: 'ion:home-outline',
title: '首页',
url: '/',
},
{
color: '#bf0c2c',
icon: 'ion:grid-outline',
title: '仪表盘',
url: '/dashboard',
},
{
color: '#e18525',
icon: 'ion:layers-outline',
title: '组件',
url: '/demos/features/icons',
},
{
color: '#3fb27f',
icon: 'ion:settings-outline',
title: '系统管理',
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
},
{
color: '#4daf1bc9',
icon: 'ion:key-outline',
title: '权限管理',
url: '/demos/access/page-control',
},
{
color: '#00d8ff',
icon: 'ion:bar-chart-outline',
title: '图表',
url: '/analytics',
},
];
const todoItems = ref<WorkbenchTodoItem[]>([
{
completed: false,
content: `审查最近提交到Git仓库的前端代码确保代码质量和规范。`,
date: '2024-07-30 11:00:00',
title: '审查前端代码提交',
},
{
completed: true,
content: `检查并优化系统性能降低CPU使用率。`,
date: '2024-07-30 11:00:00',
title: '系统性能优化',
},
{
completed: false,
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
date: '2024-07-30 11:00:00',
title: '安全检查',
},
{
completed: false,
content: `更新项目中的所有npm依赖包确保使用最新版本。`,
date: '2024-07-30 11:00:00',
title: '更新项目依赖',
},
{
completed: false,
content: `修复用户报告的页面UI显示问题确保在不同浏览器中显示一致。 `,
date: '2024-07-30 11:00:00',
title: '修复UI显示问题',
},
]);
const trendItems: WorkbenchTrendItem[] = [
{
avatar: 'svg:avatar-1',
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
date: '刚刚',
title: '威廉',
},
{
avatar: 'svg:avatar-2',
content: `关注了 <a>威廉</a> `,
date: '1个小时前',
title: '艾文',
},
{
avatar: 'svg:avatar-3',
content: `发布了 <a>个人动态</a> `,
date: '1天前',
title: '克里斯',
},
{
avatar: 'svg:avatar-4',
content: `发表文章 <a>如何编写一个Vite插件</a> `,
date: '2天前',
title: 'Vben',
},
{
avatar: 'svg:avatar-1',
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
date: '3天前',
title: '皮特',
},
{
avatar: 'svg:avatar-2',
content: `关闭了问题 <a>如何运行项目</a> `,
date: '1周前',
title: '杰克',
},
{
avatar: 'svg:avatar-3',
content: `发布了 <a>个人动态</a> `,
date: '1周前',
title: '威廉',
},
{
avatar: 'svg:avatar-4',
content: `推送了代码到 <a>Github</a>`,
date: '2021-04-01 20:00',
title: '威廉',
},
{
avatar: 'svg:avatar-4',
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
date: '2021-03-01 20:00',
title: 'Vben',
},
];
const router = useRouter();
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
// This is a sample method, adjust according to the actual project requirements
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
if (nav.url?.startsWith('http')) {
openWindow(nav.url);
return;
}
if (nav.url?.startsWith('/')) {
router.push(nav.url).catch((error) => {
console.error('Navigation failed:', error);
});
} else {
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
}
}
</script>
<template>
<div class="p-5">
<WorkbenchHeader
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
>
<template #title>
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧
</template>
<template #description> 今日晴20 - 32 </template>
</WorkbenchHeader>
<div class="mt-5 flex flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-3/5">
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
</div>
<div class="w-full lg:w-2/5">
<WorkbenchQuickNav
:items="quickNavItems"
class="mt-5 lg:mt-0"
title="快捷导航"
@click="navTo"
/>
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
<AnalysisChartCard class="mt-5" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,264 @@
<script lang="ts" setup>
import type { VbenFormProps } from "#/adapter/form";
import type { VxeTableGridOptions } from "#/adapter/vxe-table";
import { Page } from "@vben/common-ui";
import { useVbenVxeGrid } from "#/adapter/vxe-table";
import {
getBagListApi,
getBagTotalOfToBeCheckedApi,
getLevel1TagApi,
getStatusApi,
} from "#/api/core/baglist";
import { useRouter } from "vue-router";
import { ref, onMounted } from "vue";
const router = useRouter();
interface RowType {
id: any;
file_name: string;
capture_datetime: string;
bag_status: number;
level1_tag: string;
status: string;
create_time: string;
}
const formOptions = ref<VbenFormProps>({
// 默认展开
collapsed: false,
schema: [
{
component: "Input",
defaultValue: "",
fieldName: "file_name",
label: "Bag name",
componentProps: {
allowClear: true,
options: [],
placeholder: "请选择bag名称",
},
},
{
component: "Select",
fieldName: "level1_tag",
label: "STS name",
componentProps: {
allowClear: true,
options: [],
placeholder: "请选择一级标签",
},
},
{
component: "Select",
fieldName: "status",
label: "Status",
componentProps: {
allowClear: true,
options: [],
placeholder: "请选择处理状态",
},
},
{
component: "DatePicker",
defaultValue: "",
fieldName: "create_time",
label: "Creation date",
componentProps: {
type: "daterange",
rangeSeparator: "To",
startPlaceholder: "开始时间",
endPlaceholder: "结束时间",
format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD",
clearable: true,
size: "default",
style: { width: "100%" },
},
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
});
// 使用 ref 包装 gridOptions
const gridOptions = ref<VxeTableGridOptions<RowType>>({
rowConfig: {
isHover: true,
},
checkboxConfig: {
highlight: true,
labelField: "name",
},
columns: [
{ title: "ID", type: "seq", width: 50 },
{
field: "create_time",
title: "Creation date",
},
{
field: "file_name",
title: "Bag name",
},
{
field: "level1_tag",
title: "STS name",
},
{
field: "bag_status",
title: "Verifcation",
filterMultiple: false,
cellRender: {
name: "CellStatus",
props: {
mode: "icon",
options: (params: any) => {
const { row } = params;
const isUpdated = row.bag_status === 1;
return [
{
content: isUpdated ? "Update" : "Start",
name: isUpdated ? "Update" : "Start",
status: isUpdated ? "primary" : "success",
visible: true,
disabled: false,
onClick: (action: any, row: any) => {
console.log(12345, row);
if (!isUpdated) {
router.push({
name: "Detail",
params: { id: row.id },
});
} else {
router.push({
name: "DetailUpdated",
params: { id: row.id },
});
}
},
},
];
},
},
},
},
{
field: "status",
title: "Status",
},
],
exportConfig: {},
height: "auto",
keepSource: true,
pagerConfig: {
pageSizes: [10, 20, 50, 100],
},
proxyConfig: {
response: {
result: "data",
total: "total",
},
ajax: {
query: async ({ page }, formValues) => {
// 处理日期范围
let dateParams = {};
if (formValues.create_time && Array.isArray(formValues.create_time)) {
dateParams = {
start_datetime: formValues.create_time[0],
end_datetime: formValues.create_time[1],
};
}
// 移除日期字段
const { create_time, ...restFormValues } = formValues || {};
// 合并所有参数
const queryParams = {
page: page.currentPage,
per_page: page.pageSize,
...restFormValues,
...dateParams,
};
// console.log("最终发送给后端的参数:", queryParams);
const res = await getBagListApi(queryParams);
return res;
},
},
},
});
// 使用 useVbenVxeGrid
const [Grid] = useVbenVxeGrid({
formOptions: formOptions.value,
gridOptions: gridOptions.value,
});
// 动态加载 STS 标签
async function getLevel1() {
try {
const res = await getLevel1TagApi();
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === "level1_tag"
);
if (stsSelectItem && stsSelectItem.componentProps) {
// 动态追加选项
stsSelectItem.componentProps.options = res.map((item) => ({
label: item.name, // 假设接口返回name作为显示文本
value: item.id, // 假设接口返回id作为值
}));
}
} catch (error) {
console.error("获取 level1 标签失败:", error);
}
}
// 动态加载 status 状态选项
async function getStatus() {
try {
const res = await getStatusApi();
// console.log('res', res);
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === "status"
);
if (stsSelectItem && stsSelectItem.componentProps) {
// 动态追加选项
stsSelectItem.componentProps.options = res.map((item) => ({
label: item.label, // 假设接口返回name作为显示文本
value: item.label, // 假设接口返回id作为值
}));
}
} catch (error) {
console.error("获取 level1 标签失败:", error);
}
}
// 定义 total 变量
const total = ref(0);
async function fecthTobeCheckTotal() {
const res = await getBagTotalOfToBeCheckedApi();
total.value = res.data.total_tobe_checked;
}
// 初始化加载数据
onMounted(() => {
getLevel1();
getStatus();
fecthTobeCheckTotal();
});
</script>
<template>
<Page autoContentHeight :contentClass="'p-3'">
<Grid v-bind="gridOptions">
<template #toolbar-tools>
<strong>待标注数据总数: {{ total }}</strong>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,409 @@
<script lang="ts" setup>
import type { VbenFormProps } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getRetestBagListApi,
getLevel1TagApi,
getStatusApi,
} from '#/api/core/baglist';
import { useRouter } from 'vue-router';
import { ref, onMounted } from 'vue';
import { getUsersApi } from '#/api/core/user';
import { getFinishListApi } from '#/api/core/qabag';
const router = useRouter();
interface RowType {
id: any;
file_name: string;
capture_datetime: string;
bag_status: number;
level1_tag: string;
status: string;
create_time: string;
}
const formOptions = ref<VbenFormProps>({
// 默认展开
collapsed: false,
schema: [
{
component: 'Input',
defaultValue: '',
fieldName: 'file_name',
label: 'Bag name',
componentProps: {
allowClear: true,
options: [],
placeholder: '请输入bag名称',
},
},
{
component: 'Select',
fieldName: 'level1_tag',
label: 'STS name',
componentProps: {
allowClear: true,
options: [],
placeholder: '请选择一级标签',
},
},
{
component: 'Select',
fieldName: 'status',
label: 'Status',
componentProps: {
allowClear: true,
options: [],
placeholder: '请选择处理状态',
},
},
{
component: 'DatePicker',
defaultValue: '',
fieldName: 'create_time',
label: 'Creation date',
componentProps: {
type: 'daterange',
rangeSeparator: 'To',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
clearable: true,
size: 'default',
style: { width: '100%' },
},
},
{
component: 'Select',
fieldName: 'user_id',
label: 'Username',
componentProps: {
allowClear: true,
options: [],
placeholder: '请选择用户',
},
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
});
// 使用 ref 包装 gridOptions
const gridOptions = ref<VxeTableGridOptions<RowType>>({
rowConfig: {
isHover: true,
},
checkboxConfig: {
highlight: true, // 选中行高亮
labelField: 'id', // 绑定行数据的唯一标识字段建议使用id
trigger: 'row', // 点击行即可选中复选框
// showIndeterminate: true, // 显示半选状态(用于全选功能)
checkRowKeys: [], // 初始选中的行ID数组可选
},
columns: [
{ title: 'ID', type: 'seq', width: 50 },
{
field: 'create_time',
title: 'creation date',
},
{
field: 'file_name',
title: 'Bag name',
},
{
field: 'level1_tag',
title: 'STS name',
},
{
field: 'comment1',
title: 'Comment',
},
{
field: 'bag_status',
title: 'Verification',
filterMultiple: false,
cellRender: {
name: 'CellStatus',
props: {
mode: 'icon',
options: (params: any) => {
const { row } = params;
const isUpdated = row.bag_status >= 1;
return [
{
content: isUpdated ? 'Update' : 'Start',
name: isUpdated ? 'Update' : 'Start',
status: isUpdated ? 'primary' : 'success',
visible: true,
disabled: false,
onClick: (action: any, row: any) => {
if (!isUpdated) {
router.push({
name: 'Detail',
params: { id: row.id },
});
} else {
router.push({
name: 'DetailUpdated',
params: { id: row.id },
});
}
},
},
];
},
},
},
},
{
field: 'qa_status',
title: 'QA status',
filterMultiple: false,
cellRender: {
name: 'CellStatus',
props: {
mode: 'icon',
options: (params: any) => {
const { row } = params;
const qaStatus = row.qa_status;
// 定义三种状态映射
const statusMap = {
'QA_NOT_REVIEWED': { // 未处理
content: 'NOT_REVIEWED',
name: '未处理',
status: 'danger', // 红色
icon: 'exclamation-circle', // 图标名称(根据实际调整)
},
'QA_PASSED': { // 已通过
content: 'PASSED',
name: '已通过',
status: 'success', // 绿色
icon: 'check-circle', // 对勾图标
},
'QA_MODIFY': { // 已失败
content: 'MODIFY',
name: '已失败',
status: 'warning', // 红色
icon: 'close-circle', // 关闭图标
},
'QA_INVALID': { // 已失败
content: 'INVALID',
name: '已失败',
status: 'info', // 红色
icon: 'close-circle', // 关闭图标
}
};
// 根据 qa_status 获取对应配置
const config = statusMap[qaStatus] || {
content: '未知状态',
name: '未知',
status: 'default', // 默认灰色
icon: 'question-circle',
};
return [config];
},
},
},
},
{
field: 'sync_status',
title: 'Sync status',
filterMultiple: false,
cellRender: {
name: 'CellStatus',
props: {
mode: 'icon',
options: (params: any) => {
const { row } = params;
const isUpdated = row.qa_status;
return [
{
content: isUpdated == 'SYNC_NOT_READY' ? '未发送' : '已发送',
name: isUpdated == 'SYNC_NOT_READY' ? '未发送' : '已发送',
status: isUpdated == 'SYNC_NOT_READY' ? 'danger' : 'info',
visible: true,
disabled: false,
},
];
},
},
},
},
{
field: 'status',
title: 'Status',
},
{
field: 'update_time',
title: 'Update Time',
},
{
field: 'username',
title: 'User',
},
{
field: 'qa_username',
title: 'QA User',
},
],
exportConfig: {},
height: 'auto',
keepSource: true,
pagerConfig: {
pageSizes: [10, 20, 50, 100],
},
proxyConfig: {
response: {
result: 'data',
total: 'total',
},
ajax: {
query: async ({ page }, formValues) => {
// 处理日期范围
let dateParams = {};
if (
formValues.create_time &&
Array.isArray(formValues.create_time)
) {
dateParams = {
start_datetime: formValues.create_time[0],
end_datetime: formValues.create_time[1],
};
}
// 移除日期字段
const { create_time, ...restFormValues } = formValues || {};
// 合并所有参数
const queryParams = {
page: page.currentPage,
per_page: page.pageSize,
...restFormValues,
...dateParams,
};
// console.log('最终发送给后端的参数:', queryParams);
const res = await getFinishListApi(queryParams);
// console.log('1123:', res.data);
return res;
},
},
},
});
// 工具栏按钮点击事件处理
const handleToolbarClick = (params: any) => {
const { code } = params;
const selectedIds = gridInstance.value.getCheckboxRowKeys();
const selectedRows = gridInstance.value.getCheckboxRecords();
switch (code) {
case 'getSelected':
console.log('选中的ID:', selectedIds);
console.log('选中的行数据:', selectedRows);
break;
case 'batchDelete':
if (selectedIds.length === 0) {
console.warn('请先选择要删除的项');
return;
}
console.log('待删除的ID:', selectedIds);
// 调用删除接口
break;
}
};
// 使用 useVbenVxeGrid
const [Grid] = useVbenVxeGrid({
formOptions: formOptions.value,
gridOptions: gridOptions.value,
events: {
// 绑定工具栏按钮点击事件
toolbarButtonClick: handleToolbarClick,
},
});
// 动态加载 STS 标签
async function getLevel1() {
try {
const res = await getLevel1TagApi();
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === 'level1_tag',
);
if (stsSelectItem && stsSelectItem.componentProps) {
// 动态追加选项
stsSelectItem.componentProps.options = res.map((item) => ({
label: item.name, // 假设接口返回name作为显示文本
value: item.id, // 假设接口返回id作为值
}));
}
} catch (error) {
console.error('获取 level1 标签失败:', error);
}
}
// 动态加载 status 状态选项
async function getStatus() {
try {
const res = await getStatusApi();
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === 'status',
);
if (stsSelectItem && stsSelectItem.componentProps) {
// 动态追加选项
stsSelectItem.componentProps.options = res.map((item) => ({
label: item.label, // 假设接口返回name作为显示文本
value: item.label, // 假设接口返回id作为值
}));
}
} catch (error) {
console.error('获取 level1 标签失败:', error);
}
}
// 动态加载 User 标签
async function getUser() {
try {
const res = await getUsersApi();
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === 'user_id',
);
if (stsSelectItem && stsSelectItem.componentProps) {
// 动态追加选项
stsSelectItem.componentProps.options = res.map((item) => ({
label: item.username, // 假设接口返回name作为显示文本
value: item.id, // 假设接口返回id作为值
}));
}
} catch (error) {
console.error('获取 level1 标签失败:', error);
}
}
// 初始化加载数据
onMounted(() => {
getLevel1();
getStatus();
getUser();
});
// v-on="gridEvents"
</script>
<template>
<Page autoContentHeight :contentClass="'p-3'">
<Grid v-bind="gridOptions" />
</Page>
</template>

View File

@@ -0,0 +1,635 @@
<script lang="ts" setup>
import type { VbenFormProps } from "#/adapter/form";
import type { VxeGridListeners, VxeTableGridOptions } from "#/adapter/vxe-table";
import { Page } from "@vben/common-ui";
import { useVbenVxeGrid } from "#/adapter/vxe-table";
import {
getRetestBagListApi,
getLevel1TagApi,
getStatusApi,
getExportRetestBagListApi,
} from "#/api/core/baglist";
import { useRouter } from "vue-router";
import { ref, onMounted } from "vue";
import { getUserInfoApi, getUsersApi } from "#/api/core/user";
import { insertDbIdsApi, insertRootDbApi } from "#/api/core/qabag";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
const router = useRouter();
// 2. 缓存表单值
const currentFormValues = ref({});
interface RowType {
front_end_sec: any;
front_start_sec: null;
id: any;
file_name: string;
capture_datetime: string;
bag_status: number;
level1_tag: string;
status: string;
create_time: string;
}
const formOptions = ref<VbenFormProps>({
// 默认展开
collapsed: false,
schema: [
{
component: "Input",
defaultValue: "",
fieldName: "file_name",
label: "Bag name",
componentProps: {
allowClear: true,
options: [],
placeholder: "请输入bag名称",
},
},
{
component: "Select",
fieldName: "level1_tag",
label: "STS name",
componentProps: {
allowClear: true,
options: [],
placeholder: "请选择一级标签",
},
},
{
component: "Select",
fieldName: "status",
label: "Status",
componentProps: {
allowClear: true,
options: [],
placeholder: "请选择处理状态",
},
},
{
component: "DatePicker",
defaultValue: "",
fieldName: "update_time",
label: "Update date",
componentProps: {
type: "daterange",
rangeSeparator: "To",
startPlaceholder: "开始时间",
endPlaceholder: "结束时间",
format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD",
clearable: true,
size: "default",
style: { width: "100%" },
},
},
{
component: "Select",
fieldName: "user_id",
label: "Username",
componentProps: {
allowClear: true,
options: [],
placeholder: "请选择标注用户",
},
},
{
component: "Select",
fieldName: "qa_status",
label: "QA status",
componentProps: {
allowClear: true,
options: [
{
label: "QA_NOT_REVIEWED",
value: "QA_NOT_REVIEWED",
},
{
label: "QA_PASSED",
value: "QA_PASSED",
},
{
label: "QA_MODIFY",
value: "QA_MODIFY",
},
{
label: "QA_INVALID",
value: "QA_INVALID",
},
],
placeholder: "请选择复检状态",
},
},
{
component: "Select",
fieldName: "qa_id",
label: "QA user",
componentProps: {
allowClear: true,
options: [],
placeholder: "请选择复检用户",
},
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
});
// 使用 ref 包装 gridOptions
const gridOptions = ref<VxeTableGridOptions<RowType>>({
rowConfig: {
isHover: true,
},
checkboxConfig: {
highlight: true, // 选中行高亮
labelField: "id", // 绑定行数据的唯一标识字段建议使用id
trigger: "row", // 点击行即可选中复选框
// showIndeterminate: true, // 显示半选状态(用于全选功能)
checkRowKeys: [], // 初始选中的行ID数组可选
},
columns: [
{
title: "ID",
type: "checkbox",
width: 100, // 复选框列宽度
fixed: "left", // 固定在左侧(可选)
align: "left",
},
{
field: "file_name",
title: "Bag name",
},
{
field: "level1_tag",
title: "STS name",
},
{
field: "comment1",
title: "Comment",
},
{
field: "bag_status",
title: "Verification",
filterMultiple: false,
cellRender: {
name: "CellStatus",
props: {
mode: "icon",
options: (params: any) => {
const { row } = params;
const isUpdated = row.bag_status >= 1;
return [
{
content: isUpdated ? "Update" : "Start",
name: isUpdated ? "Update" : "Start",
status: isUpdated ? "primary" : "success",
visible: true,
disabled: false,
onClick: (action: any, row: any) => {
// console.log(12345, row);
if (!isUpdated) {
router.push({
name: "Detail",
params: { id: row.id },
});
} else {
const routeData = router.resolve({
name: "DetailUpdated",
params: { id: row.id },
});
window.open(routeData.href, "_blank");
}
},
},
];
},
},
},
},
{
field: "qa_status",
title: "QA status",
filterMultiple: false,
cellRender: {
name: "CellStatus",
props: {
mode: "icon",
options: (params: any) => {
const { row } = params;
const qaStatus = row.qa_status;
// 定义三种状态映射
const statusMap = {
QA_NOT_REVIEWED: {
// 未处理
content: "NOT_REVIEWED",
name: "未处理",
status: "danger", // 红色
icon: "exclamation-circle", // 图标名称(根据实际调整)
},
QA_PASSED: {
// 已通过
content: "PASSED",
name: "已通过",
status: "success", // 绿色
icon: "check-circle", // 对勾图标
},
QA_MODIFY: {
// 已失败
content: "MODIFY",
name: "已失败",
status: "warning", // 红色
icon: "close-circle", // 关闭图标
},
QA_INVALID: {
// 已失败
content: "INVALID",
name: "已失败",
status: "info", // 红色
icon: "close-circle", // 关闭图标
},
};
// 根据 qa_status 获取对应配置
const config = statusMap[qaStatus] || {
content: "未知状态",
name: "未知",
status: "default", // 默认灰色
icon: "question-circle",
};
return [config];
},
},
},
},
{
field: "status",
title: "Status",
},
{
field: "caseTime",
title: "Case time",
// 新增 formatter 属性
formatter: ({ row }) => {
// 假设后端返回的字段名为 front_start_sec 和 front_end_sec
const start = row.front_start_sec !== null ? row.front_start_sec : 0;
const end = row.front_end_sec !== null ? row.front_end_sec : 0;
return `${start},${end}`;
},
},
{
field: "update_time",
title: "Update Time",
},
{
field: "qa_time",
title: "QA Time",
},
{
field: "username",
title: "User",
},
{
field: "qa_username",
title: "QA User",
},
],
exportConfig: {},
height: "auto",
keepSource: true,
pagerConfig: {
pageSizes: [10, 20, 50, 100],
},
proxyConfig: {
response: {
result: "data",
total: "total",
},
ajax: {
query: async ({ page }, formValues) => {
// 更新缓存
currentFormValues.value = formValues || {};
// // 保存status筛选条件到localStorage
// if (formValues?.status) {
// localStorage.setItem('statusFilterValue', formValues.status);
// } else {
// localStorage.removeItem('statusFilterValue');
// }
// 处理日期范围
let dateParams = {};
if (formValues.create_time && Array.isArray(formValues.create_time)) {
dateParams = {
start_datetime: formValues.create_time[0],
end_datetime: formValues.create_time[1],
};
}
// 移除日期字段
const { create_time, ...restFormValues } = formValues || {};
// 合并所有参数
const queryParams = {
page: page.currentPage,
per_page: page.pageSize,
...restFormValues,
...dateParams,
};
// console.log("最终发送给后端的参数:", queryParams);
const res = await getRetestBagListApi(queryParams);
// console.log('1123:', res.data);
return res;
},
},
},
});
// 工具栏按钮点击事件处理
const handleToolbarClick = (params: any) => {
const { code } = params;
const selectedIds = gridInstance.value.getCheckboxRowKeys();
const selectedRows = gridInstance.value.getCheckboxRecords();
switch (code) {
case "getSelected":
console.log("选中的ID:", selectedIds);
console.log("选中的行数据:", selectedRows);
break;
case "batchDelete":
if (selectedIds.length === 0) {
console.warn("请先选择要删除的项");
return;
}
console.log("待删除的ID:", selectedIds);
// 调用删除接口
break;
}
};
// 使用 useVbenVxeGrid
const [Grid, gridInstance] = useVbenVxeGrid({
formOptions: formOptions.value,
gridOptions: gridOptions.value,
events: {
// 绑定工具栏按钮点击事件
toolbarButtonClick: handleToolbarClick,
},
});
// 动态加载 STS 标签
async function getLevel1() {
try {
const res = await getLevel1TagApi();
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === "level1_tag"
);
if (stsSelectItem && stsSelectItem.componentProps) {
// 动态追加选项
stsSelectItem.componentProps.options = res.map((item) => ({
label: item.name, // 假设接口返回name作为显示文本
value: item.id, // 假设接口返回id作为值
}));
}
} catch (error) {
console.error("获取 level1 标签失败:", error);
}
}
// 动态加载 status 状态选项
async function getStatus() {
try {
const res = await getStatusApi();
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === "status"
);
if (stsSelectItem && stsSelectItem.componentProps) {
// 动态追加选项
stsSelectItem.componentProps.options = res.map((item) => ({
label: item.label, // 假设接口返回name作为显示文本
value: item.label, // 假设接口返回id作为值
}));
}
} catch (error) {
console.error("获取 status 失败:", error);
}
}
// 动态加载 标注 User 标签
async function getUser() {
try {
const res = await getUsersApi();
const stsSelectItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === "user_id"
);
const stsSelectQaItem = formOptions.value?.schema.find(
(item: any) => item.fieldName === "qa_id"
);
if (stsSelectItem && stsSelectItem.componentProps) {
stsSelectItem.componentProps.options = res
.filter((item: any) => item.name === "user") // Only keep user items
.map((item: any) => ({
label: item.username,
value: item.id,
}));
}
if (stsSelectQaItem && stsSelectQaItem.componentProps) {
stsSelectQaItem.componentProps.options = res
.filter((item: any) => item.name === "check") // Only keep user items
.map((item: any) => ({
label: item.username,
value: item.id,
}));
}
} catch (error) {
console.error("获取 level1 标签失败:", error);
}
}
const sendForm = ref([]);
// const idS = ref([]);
// 事件监听
const gridEvents: VxeGridListeners<RowType> = {
checkboxChange: ({ records }) => {
// console.log("当前页选中:", records);
sendForm.value = convertData(records);
// idS.value = records.map((item) => item.id);
},
checkboxAll: ({ records }) => {
// console.log("全选状态:", records);
sendForm.value = convertData(records);
// idS.value = records.map((item) => item.id);
},
};
function convertData(originalData) {
return originalData.map((item) => {
return {
bag_name: item.file_name,
rowid: item.id,
};
});
}
async function sendToDB() {
// 前置验证
if (!sendForm.value?.length) {
ElMessage.warning("请先选择有效数据");
return;
}
try {
// 添加确认对话框
await ElMessageBox.confirm(
"确认要将数据同步到 RootDB 吗?此操作不可逆!",
"同步确认",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
beforeClose: (action, instance, done) => {
if (action === "confirm") {
// 禁用按钮防止重复点击
instance.confirmButtonLoading = true;
}
done();
},
}
);
// 用户点击确认后执行同步操作
const loading = ElLoading.service({
lock: true,
text: "同步中...",
background: "rgba(0,0,0,0.5)",
});
try {
const res = await insertRootDbApi(sendForm.value);
// await insertDbIdsApi(idS.value);
// 刷新表格数据
await gridInstance.reload();
ElMessage.success({
message: res.message,
duration: 3000,
});
} finally {
loading.close();
}
} catch (error) {
// 用户点击取消或关闭对话框
if (error === "cancel") {
ElMessage.info("同步已取消");
} else {
ElMessage.error(`同步失败:${error instanceof Error ? error.message : "未知错误"}`);
}
}
}
async function exportFile() {
try {
const response = await getExportRetestBagListApi(currentFormValues.value);
// 文件下载处理...
const blob = new Blob([response], {
type: "text/csv;charset=utf-8;",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
const shortTimestamp = String(new Date().getTime()).slice(-6); // 截取后6位
link.setAttribute("download", `retest_data_${shortTimestamp}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
ElMessage.success("文件下载成功");
} catch (error) {
ElMessage.error(`下载失败:${error || "未知错误"}`);
}
}
const LdUserName = ref("");
async function fetchUserInfo() {
const res = await getUserInfoApi();
LdUserName.value = res.realName;
}
// 初始化加载数据
onMounted(() => {
getLevel1();
getStatus();
getUser();
fetchUserInfo();
});
</script>
<template>
<Page autoContentHeight :contentClass="'p-3'">
<Grid v-bind="gridOptions" :grid-events="gridEvents" @checkbox-change="" @checkbox-all="">
<template #toolbar-tools>
<div v-if="LdUserName != 'LD_tianyuhang' && LdUserName != 'LD_zhaojingwei'">
<el-button @click="exportFile" type="success">导出</el-button>
<el-button @click="sendToDB" type="primary">Sync to Root DB</el-button>
</div>
</template>
</Grid>
</Page>
</template>
<style scoped>
/* .el-button {
position: relative;
overflow: hidden;
padding: 8px 15px !important;
border-radius: 4px;
border: 1px solid #409eff;
background: #409eff;
color: #fff;
transition: all 0.3s;
} */
/* .el-button--primary:hover {
background: #66b1ff;
border-color: #66b1ff;
} */
.el-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.is-loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
border: 2px solid #fff;
border-radius: 50%;
border-top: none;
border-right: none;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) rotate(45deg);
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: translate(-50%, -50%) rotate(405deg);
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More