提交
This commit is contained in:
5
apps/web-ele/.env
Normal file
5
apps/web-ele/.env
Normal file
@@ -0,0 +1,5 @@
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=FST Data Factory
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=FST-Data-Factory
|
||||
7
apps/web-ele/.env.analyze
Normal file
7
apps/web-ele/.env.analyze
Normal file
@@ -0,0 +1,7 @@
|
||||
# public path
|
||||
VITE_BASE=/
|
||||
|
||||
# Basic interface address SPA
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
VITE_VISUALIZER=true
|
||||
16
apps/web-ele/.env.development
Normal file
16
apps/web-ele/.env.development
Normal file
@@ -0,0 +1,16 @@
|
||||
# 端口号
|
||||
VITE_PORT=5777
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||
VITE_NITRO_MOCK=false
|
||||
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
23
apps/web-ele/.env.production
Normal file
23
apps/web-ele/.env.production
Normal 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
41
apps/web-ele/components.d.ts
vendored
Normal 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
35
apps/web-ele/index.html
Normal 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
56
apps/web-ele/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
apps/web-ele/postcss.config.mjs
Normal file
1
apps/web-ele/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
BIN
apps/web-ele/public/favicon1.ico
Normal file
BIN
apps/web-ele/public/favicon1.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
1
apps/web-ele/public/icon/back.svg
Normal file
1
apps/web-ele/public/icon/back.svg
Normal 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 |
BIN
apps/web-ele/public/image/logo.png
Normal file
BIN
apps/web-ele/public/image/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
234
apps/web-ele/src/adapter/component/index.ts
Normal file
234
apps/web-ele/src/adapter/component/index.ts
Normal 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 };
|
||||
39
apps/web-ele/src/adapter/form.ts
Normal file
39
apps/web-ele/src/adapter/form.ts
Normal 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 };
|
||||
197
apps/web-ele/src/adapter/vxe-table.ts
Normal file
197
apps/web-ele/src/adapter/vxe-table.ts
Normal 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';
|
||||
51
apps/web-ele/src/api/core/auth.ts
Normal file
51
apps/web-ele/src/api/core/auth.ts
Normal 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');
|
||||
}
|
||||
74
apps/web-ele/src/api/core/baglist.ts
Normal file
74
apps/web-ele/src/api/core/baglist.ts
Normal 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' });
|
||||
}
|
||||
3
apps/web-ele/src/api/core/index.ts
Normal file
3
apps/web-ele/src/api/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
25
apps/web-ele/src/api/core/label.ts
Normal file
25
apps/web-ele/src/api/core/label.ts
Normal 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);
|
||||
}
|
||||
10
apps/web-ele/src/api/core/menu.ts
Normal file
10
apps/web-ele/src/api/core/menu.ts
Normal 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');
|
||||
}
|
||||
21
apps/web-ele/src/api/core/qabag.ts
Normal file
21
apps/web-ele/src/api/core/qabag.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
19
apps/web-ele/src/api/core/rootdata.ts
Normal file
19
apps/web-ele/src/api/core/rootdata.ts
Normal 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 });
|
||||
}
|
||||
|
||||
26
apps/web-ele/src/api/core/user.ts
Normal file
26
apps/web-ele/src/api/core/user.ts
Normal 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);
|
||||
}
|
||||
38
apps/web-ele/src/api/core/vlm.ts
Normal file
38
apps/web-ele/src/api/core/vlm.ts
Normal 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);
|
||||
}
|
||||
1
apps/web-ele/src/api/index.ts
Normal file
1
apps/web-ele/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './core';
|
||||
137
apps/web-ele/src/api/request.ts
Normal file
137
apps/web-ele/src/api/request.ts
Normal 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
17
apps/web-ele/src/app.vue
Normal 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>
|
||||
65
apps/web-ele/src/bootstrap.ts
Normal file
65
apps/web-ele/src/bootstrap.ts
Normal 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 };
|
||||
24
apps/web-ele/src/layouts/auth.vue
Normal file
24
apps/web-ele/src/layouts/auth.vue
Normal 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>
|
||||
157
apps/web-ele/src/layouts/basic.vue
Normal file
157
apps/web-ele/src/layouts/basic.vue
Normal 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>
|
||||
6
apps/web-ele/src/layouts/index.ts
Normal file
6
apps/web-ele/src/layouts/index.ts
Normal 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 };
|
||||
3
apps/web-ele/src/locales/README.md
Normal file
3
apps/web-ele/src/locales/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# locale
|
||||
|
||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||
100
apps/web-ele/src/locales/index.ts
Normal file
100
apps/web-ele/src/locales/index.ts
Normal 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 };
|
||||
13
apps/web-ele/src/locales/langs/en-US/demos.json
Normal file
13
apps/web-ele/src/locales/langs/en-US/demos.json
Normal 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"
|
||||
}
|
||||
}
|
||||
14
apps/web-ele/src/locales/langs/en-US/page.json
Normal file
14
apps/web-ele/src/locales/langs/en-US/page.json
Normal 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"
|
||||
}
|
||||
}
|
||||
13
apps/web-ele/src/locales/langs/zh-CN/demos.json
Normal file
13
apps/web-ele/src/locales/langs/zh-CN/demos.json
Normal 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 版本"
|
||||
}
|
||||
}
|
||||
25
apps/web-ele/src/locales/langs/zh-CN/page.json
Normal file
25
apps/web-ele/src/locales/langs/zh-CN/page.json
Normal 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
31
apps/web-ele/src/main.ts
Normal 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();
|
||||
26
apps/web-ele/src/preferences.ts
Normal file
26
apps/web-ele/src/preferences.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
42
apps/web-ele/src/router/access.ts
Normal file
42
apps/web-ele/src/router/access.ts
Normal 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 };
|
||||
139
apps/web-ele/src/router/guard.ts
Normal file
139
apps/web-ele/src/router/guard.ts
Normal 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 };
|
||||
37
apps/web-ele/src/router/index.ts
Normal file
37
apps/web-ele/src/router/index.ts
Normal 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 };
|
||||
88
apps/web-ele/src/router/routes/core.ts
Normal file
88
apps/web-ele/src/router/routes/core.ts
Normal 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 };
|
||||
37
apps/web-ele/src/router/routes/index.ts
Normal file
37
apps/web-ele/src/router/routes/index.ts
Normal 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 };
|
||||
45
apps/web-ele/src/router/routes/modules/dashboard.ts
Normal file
45
apps/web-ele/src/router/routes/modules/dashboard.ts
Normal 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;
|
||||
65
apps/web-ele/src/router/routes/modules/datamanage.ts
Normal file
65
apps/web-ele/src/router/routes/modules/datamanage.ts
Normal 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;
|
||||
38
apps/web-ele/src/router/routes/modules/demos.ts
Normal file
38
apps/web-ele/src/router/routes/modules/demos.ts
Normal 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;
|
||||
58
apps/web-ele/src/router/routes/modules/detailpage.ts
Normal file
58
apps/web-ele/src/router/routes/modules/detailpage.ts
Normal 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;
|
||||
44
apps/web-ele/src/router/routes/modules/labelmanage.ts
Normal file
44
apps/web-ele/src/router/routes/modules/labelmanage.ts
Normal 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;
|
||||
137
apps/web-ele/src/router/routes/modules/rootdb.ts
Normal file
137
apps/web-ele/src/router/routes/modules/rootdb.ts
Normal 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;
|
||||
51
apps/web-ele/src/router/routes/modules/usercenter.ts
Normal file
51
apps/web-ele/src/router/routes/modules/usercenter.ts
Normal 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;
|
||||
82
apps/web-ele/src/router/routes/modules/vben.ts
Normal file
82
apps/web-ele/src/router/routes/modules/vben.ts
Normal 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;
|
||||
54
apps/web-ele/src/router/routes/modules/vlm.ts
Normal file
54
apps/web-ele/src/router/routes/modules/vlm.ts
Normal 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;
|
||||
169
apps/web-ele/src/store/auth.ts
Normal file
169
apps/web-ele/src/store/auth.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
1
apps/web-ele/src/store/index.ts
Normal file
1
apps/web-ele/src/store/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth';
|
||||
68
apps/web-ele/src/store/vlmStore.ts
Normal file
68
apps/web-ele/src/store/vlmStore.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
3
apps/web-ele/src/views/_core/README.md
Normal file
3
apps/web-ele/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
9
apps/web-ele/src/views/_core/about/index.vue
Normal file
9
apps/web-ele/src/views/_core/about/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { About } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'About' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<About />
|
||||
</template>
|
||||
69
apps/web-ele/src/views/_core/authentication/code-login.vue
Normal file
69
apps/web-ele/src/views/_core/authentication/code-login.vue
Normal 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>
|
||||
@@ -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>
|
||||
102
apps/web-ele/src/views/_core/authentication/login.vue
Normal file
102
apps/web-ele/src/views/_core/authentication/login.vue
Normal 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.');
|
||||
}
|
||||
10
apps/web-ele/src/views/_core/authentication/qrcode-login.vue
Normal file
10
apps/web-ele/src/views/_core/authentication/qrcode-login.vue
Normal 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>
|
||||
96
apps/web-ele/src/views/_core/authentication/register.vue
Normal file
96
apps/web-ele/src/views/_core/authentication/register.vue
Normal 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>
|
||||
7
apps/web-ele/src/views/_core/fallback/coming-soon.vue
Normal file
7
apps/web-ele/src/views/_core/fallback/coming-soon.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
9
apps/web-ele/src/views/_core/fallback/forbidden.vue
Normal file
9
apps/web-ele/src/views/_core/fallback/forbidden.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback403Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="403" />
|
||||
</template>
|
||||
9
apps/web-ele/src/views/_core/fallback/internal-error.vue
Normal file
9
apps/web-ele/src/views/_core/fallback/internal-error.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback500Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="500" />
|
||||
</template>
|
||||
9
apps/web-ele/src/views/_core/fallback/not-found.vue
Normal file
9
apps/web-ele/src/views/_core/fallback/not-found.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback404Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="404" />
|
||||
</template>
|
||||
9
apps/web-ele/src/views/_core/fallback/offline.vue
Normal file
9
apps/web-ele/src/views/_core/fallback/offline.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="offline" />
|
||||
</template>
|
||||
100
apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue
Normal file
100
apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
90
apps/web-ele/src/views/dashboard/analytics/index.vue
Normal file
90
apps/web-ele/src/views/dashboard/analytics/index.vue
Normal 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>
|
||||
266
apps/web-ele/src/views/dashboard/workspace/index.vue
Normal file
266
apps/web-ele/src/views/dashboard/workspace/index.vue
Normal 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>
|
||||
264
apps/web-ele/src/views/datamanage/datalabel/bag_table.vue
Normal file
264
apps/web-ele/src/views/datamanage/datalabel/bag_table.vue
Normal 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>
|
||||
409
apps/web-ele/src/views/datamanage/datalabel/finishprocess.vue
Normal file
409
apps/web-ele/src/views/datamanage/datalabel/finishprocess.vue
Normal 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>
|
||||
635
apps/web-ele/src/views/datamanage/datalabel/retestlabel.vue
Normal file
635
apps/web-ele/src/views/datamanage/datalabel/retestlabel.vue
Normal 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>
|
||||
169
apps/web-ele/src/views/datamanage/display/analytics-bag.vue
Normal file
169
apps/web-ele/src/views/datamanage/display/analytics-bag.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
|
||||
// 定义接收父组件的 data 属性
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 图表引用
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
// 获取 echarts 渲染方法
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
// 封装图表渲染逻辑为函数(方便复用)
|
||||
const renderChart = (data: any) => {
|
||||
// 提取统计数据的键(周数)和值
|
||||
const statisticsEntries = Object.entries(data.statistics);
|
||||
|
||||
// 1. 提取categories(第29周、第30周、第31周)
|
||||
const categories = statisticsEntries.map(([weekKey]) => {
|
||||
// 从"2025年第29周"中提取"第29周"
|
||||
return weekKey.split('年')[1];
|
||||
});
|
||||
|
||||
|
||||
// 2. 提取各状态数据
|
||||
const REVIEWED_BUT_INVALID = statisticsEntries.map(
|
||||
([_, stats]) => stats.REVIEWED_BUT_INVALID,
|
||||
);
|
||||
const MANUAL_OVERRIDE_ACCEPTED = statisticsEntries.map(
|
||||
([_, stats]) => stats.MANUAL_OVERRIDE_ACCEPTED,
|
||||
);
|
||||
const PROCESSED_NOT_REVIEWED = statisticsEntries.map(
|
||||
([_, stats]) => stats.PROCESSED_NOT_REVIEWED,
|
||||
);
|
||||
// 折线图数据(使用 otherSts 的值)
|
||||
const lineData = PROCESSED_NOT_REVIEWED;
|
||||
|
||||
// 关键:计算三个数组中的最大值
|
||||
const allValues = [
|
||||
...REVIEWED_BUT_INVALID,
|
||||
...MANUAL_OVERRIDE_ACCEPTED,
|
||||
...PROCESSED_NOT_REVIEWED
|
||||
];
|
||||
const maxValue = Math.max(...allValues);
|
||||
|
||||
// 为了让图表更美观,可在最大值基础上增加10%-20%的余量
|
||||
const yAxisMax = maxValue > 0 ? Math.ceil(maxValue * 1.2) : 10;
|
||||
|
||||
// 重新渲染图表
|
||||
renderEcharts({
|
||||
title: {
|
||||
left: 'center',
|
||||
top: 20,
|
||||
textStyle: { fontSize: 18 },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
},
|
||||
legend: {
|
||||
data: ['Accepted', 'Other STS', 'Invalid', '趋势线'],
|
||||
top: 10,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLabel: { fontSize: 14 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: yAxisMax,
|
||||
splitNumber: 4,
|
||||
axisLabel: { fontSize: 14 },
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '20%',
|
||||
containLabel: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Accepted',
|
||||
type: 'bar',
|
||||
barWidth: 20,
|
||||
barGap: '20%',
|
||||
label: { show: true, position: 'top', formatter: '{c}', color: '#000' },
|
||||
data: MANUAL_OVERRIDE_ACCEPTED,
|
||||
itemStyle: { color: '#67C23A' },
|
||||
},
|
||||
{
|
||||
name: 'Other STS',
|
||||
type: 'bar',
|
||||
barWidth: 20,
|
||||
barGap: '20%',
|
||||
label: { show: true, position: 'top', formatter: '{c}', color: '#000' },
|
||||
data: PROCESSED_NOT_REVIEWED,
|
||||
itemStyle: { color: '#FFD700' },
|
||||
},
|
||||
{
|
||||
name: 'Invalid',
|
||||
type: 'bar',
|
||||
barWidth: 20,
|
||||
barGap: '20%',
|
||||
label: { show: true, position: 'top', formatter: '{c}', color: '#000' },
|
||||
data: REVIEWED_BUT_INVALID,
|
||||
itemStyle: { color: '#F56C6C' },
|
||||
},
|
||||
{
|
||||
name: '趋势线',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
data: lineData,
|
||||
itemStyle: { color: '#409EFF' },
|
||||
lineStyle: { width: 2 },
|
||||
z: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
// 监听 props.data 变化,数据更新时重新渲染图表
|
||||
// 同时在watch中添加错误捕获
|
||||
watch(
|
||||
() => props.data,
|
||||
(newData) => {
|
||||
// console.log('子组件接收新数据:', newData);
|
||||
try {
|
||||
renderChart(newData);
|
||||
} catch (error) {
|
||||
console.error('渲染图表时出错:', error);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 组件挂载时也执行一次(可选,因为 watch 的 immediate 已包含初始渲染)
|
||||
onMounted(() => {
|
||||
// 可省略,因为 watch 已经会在初始时触发
|
||||
//console.log('子组件接收新数据23:', props.data);
|
||||
// renderChart(props.data);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" class="h-full w-full" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 确保图表容器有固定尺寸,否则可能不显示 */
|
||||
.h-full {
|
||||
height: 400px; /* 具体高度可根据需求调整 */
|
||||
}
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
100
apps/web-ele/src/views/datamanage/display/analytics-trends.vue
Normal file
100
apps/web-ele/src/views/datamanage/display/analytics-trends.vue
Normal 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>
|
||||
@@ -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>
|
||||
72
apps/web-ele/src/views/datamanage/display/dataStatistics.vue
Normal file
72
apps/web-ele/src/views/datamanage/display/dataStatistics.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="p-3">
|
||||
<HeaderPage class="rounded-md" @submit-form="handleFormSubmit"/>
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-3 rounded-md">
|
||||
<template #trends>
|
||||
<div class="mt-3 w-full">
|
||||
<analyticsBag :data="displayData" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- <template #visits>
|
||||
<div class="mt-3 w-full">
|
||||
<AnalyticsVisits />
|
||||
</div>
|
||||
</template> -->
|
||||
</AnalysisChartsTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HeaderPage from './headerPage.vue';
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
import { AnalysisChartsTabs } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
import analyticsBag from './analytics-bag.vue';
|
||||
import { ref ,onMounted} from 'vue';
|
||||
import { getBagEchartsApi } from '#/api/core/baglist'
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '数据展示',
|
||||
value: 'trends',
|
||||
},
|
||||
// {
|
||||
// label: '月处理量',
|
||||
// value: 'visits',
|
||||
// },
|
||||
];
|
||||
|
||||
const displayData = ref<any[]>([]);
|
||||
// 处理表单提交事件
|
||||
async function handleFormSubmit(formData: Record<string, any>) {
|
||||
|
||||
console.log('接收到表单数据:', formData);
|
||||
try {
|
||||
// 发送请求获取数据
|
||||
const result = await getBagEchartsApi(formData);
|
||||
// 更新展示数据
|
||||
displayData.value = result.data;
|
||||
console.log('数据获取成功:', result);
|
||||
} catch (error) {
|
||||
console.error('数据获取失败:', error);
|
||||
displayData.value = []; // 清空数据或显示错误状态
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const fetchBagEchartsApi = async () => {
|
||||
const response = await getBagEchartsApi({
|
||||
"periodValue": "",
|
||||
"tagValue": "",
|
||||
"dateSelectValue": "week3"
|
||||
});
|
||||
displayData.value=response.data;
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
fetchBagEchartsApi()
|
||||
});
|
||||
|
||||
</script>
|
||||
191
apps/web-ele/src/views/datamanage/display/headerPage.vue
Normal file
191
apps/web-ele/src/views/datamanage/display/headerPage.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div class="card-box h-48 p-4 py-4 md:h-64 lg:h-[150px]">
|
||||
<!-- 新增一个容器包裹前两个div -->
|
||||
<div class="flex flex-col lg:flex-1 lg:flex-row">
|
||||
<div class="flex items-center md:ml-8 md:mt-0">
|
||||
<h1 class="text-md mr-2 md:text-base">
|
||||
<slot name="title">最近更新</slot>
|
||||
</h1>
|
||||
<div class="ml-4 flex items-center text-blue-500 dark:text-blue-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mr-1 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-base">
|
||||
{{ lastUpdateTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-1 justify-end md:mt-0 lg:mt-0">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<span class="text-base"> 总数 </span>
|
||||
<span class="text-base">{{ BagCount.total }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mx-12 flex flex-col items-center justify-center md:mx-16">
|
||||
<span class="text-base"> 待处理 </span>
|
||||
<span class="text-base">{{ BagCount.pending }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用 flex 布局实现左右分离 -->
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-4 md:ml-8">
|
||||
<!-- 左侧:三个选择框 -->
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-1 whitespace-nowrap text-gray-700 dark:text-gray-300">时期</span>
|
||||
<el-select
|
||||
v-model="form.dateSelectValue"
|
||||
placeholder="Select Week"
|
||||
style="width: 180px"
|
||||
:disabled="!!form.periodValue"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dataSelect"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<span class="mr-1 whitespace-nowrap text-gray-700 dark:text-gray-300">标签</span>
|
||||
<el-select v-model="form.tagValue" placeholder="Select STS" style="width: 200px">
|
||||
<el-option
|
||||
v-for="item in level1Tag"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex items-center">
|
||||
<span class="mr-1 whitespace-nowrap text-gray-700 dark:text-gray-300">日期</span>
|
||||
<el-date-picker
|
||||
v-model="form.periodValue"
|
||||
type="date"
|
||||
placeholder="Select Date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
time-format="HH:mm"
|
||||
style="width: 160px"
|
||||
:disabled="!!form.dateSelectValue"
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 右侧:两个按钮 -->
|
||||
<div class="flex items-center gap-2 md:mr-16">
|
||||
<el-button type="info" @click="handleReset">
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="queryLoading" @click="submitForm">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { getBagTotalApi, getLevel1TagApi } from '#/api/core/baglist';
|
||||
|
||||
const lastUpdateTime = ref('2025-08-01 12:00:00');
|
||||
const queryLoading = ref(false); // 查询按钮加载状态
|
||||
// 定义 emit 事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'submit-form', formData: Record<string, any>): void
|
||||
}>();
|
||||
|
||||
// 使用 reactive 将多个表单字段合并为一个对象
|
||||
const form = reactive({
|
||||
periodValue: '',
|
||||
tagValue: '',
|
||||
dateSelectValue: ''
|
||||
});
|
||||
|
||||
const dataSelect = [
|
||||
{ value: 'week3', label: 'Past Week 3' },
|
||||
{ value: 'week4', label: 'Past Week 4' },
|
||||
{ value: 'week5', label: 'Past Week 5' },
|
||||
{ value: 'week6', label: 'Past Week 6' },
|
||||
{ value: 'week7', label: 'Past Week 7' },
|
||||
{ value: 'week8', label: 'Past Week 8' },
|
||||
{ value: 'week9', label: 'Past Week 9' }
|
||||
];
|
||||
|
||||
const BagCount = ref({
|
||||
total: 0,
|
||||
pending: 0,
|
||||
});
|
||||
|
||||
const level1Tag = ref([]);
|
||||
|
||||
async function fetchBagTotal() {
|
||||
const res = await getBagTotalApi();
|
||||
BagCount.value.total = res.data.total_count;
|
||||
BagCount.value.pending = res.data.zero_count;
|
||||
// console.log(44, BagCount.value.pending);
|
||||
}
|
||||
|
||||
async function fetchLevel1Tag() {
|
||||
const res = await getLevel1TagApi();
|
||||
level1Tag.value = res.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
|
||||
// 监听时期选择的变化
|
||||
watch(() => form.dateSelectValue, (newVal) => {
|
||||
if (newVal) {
|
||||
// 当选择了时期,清空具体日期
|
||||
form.periodValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 监听日期选择的变化
|
||||
watch(() => form.periodValue, (newVal) => {
|
||||
if (newVal) {
|
||||
// 当选择了具体日期,清空时期选择
|
||||
form.dateSelectValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 重置处理函数
|
||||
function handleReset() {
|
||||
// 清空所有表单字段
|
||||
form.periodValue = '';
|
||||
form.tagValue = '';
|
||||
form.dateSelectValue = '';
|
||||
}
|
||||
|
||||
|
||||
// 提交表单
|
||||
function submitForm() {
|
||||
// 触发自定义事件,将表单数据传递给父组件
|
||||
emit('submit-form', { ...form });
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
fetchBagTotal();
|
||||
fetchLevel1Tag();
|
||||
});
|
||||
|
||||
</script>
|
||||
528
apps/web-ele/src/views/datamanage/remotedata/index.vue
Normal file
528
apps/web-ele/src/views/datamanage/remotedata/index.vue
Normal file
@@ -0,0 +1,528 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex-1 p-3">
|
||||
<div class="grid grid-cols-[34%,66%]">
|
||||
<div class="flex flex-col">
|
||||
<!-- 左侧树形结构 w-1/3-->
|
||||
<div class="">
|
||||
<el-tree :data="permissionData" show-checkbox node-key="id" :default-expand-all="true" :props="defaultProps"
|
||||
@check="" class="rounded-lg tree-custom" ref="treeRef">
|
||||
<template #default="{ data }">
|
||||
<el-tooltip effect="dark" :content="`${data.label}`" placement="right" trigger="hover"
|
||||
popper-class="tree-tooltip">
|
||||
<span>{{ data.label }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-tree>
|
||||
<!-- <VbenForm :form-options="formOptions" v-model="formData" /> -->
|
||||
</div>
|
||||
<div class="mt-1 pt-2 flex justify-end gap-2">
|
||||
<el-button @click="handleCancel">重置</el-button>
|
||||
<el-button type="primary" @click="handleSave">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 bg-white rounded-lg p-0 h-[calc(100vh-120px)] ml-2">
|
||||
<!-- 右侧表格,固定高度 w-2/3 -->
|
||||
<Grid v-bind="gridOptions">
|
||||
<template #action="{ row }">
|
||||
<Button class="" type="link" @click="playVideo(row)">播放</Button>
|
||||
<Button class="" type="link" @click="handleMoreInfo(row)">更多</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="isPlayVideo" width="800px" center @closed="handleDialogClosed" :show-close="false">
|
||||
<!-- 自定义头部 -->
|
||||
<template #header="{ close, titleId }">
|
||||
<div class="my-header">
|
||||
<p :id="titleId" class="ml-2">{{ currentBagName }}</p>
|
||||
<el-button type="danger" size="small" @click="close">
|
||||
<el-icon class="el-icon--left">
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
关闭
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 视频内容区域 -->
|
||||
<div v-if="currentVideo" class="video-container">
|
||||
<div class="video-wrapper rounded-lg overflow-hidden shadow-md">
|
||||
<video ref="videoPlayer" class="w-full h-full" controls autoplay playsinline controlsList="nodownload"
|
||||
@contextmenu.prevent>
|
||||
<source :src="currentVideo" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="isMoreInfo" title="其他信息" width="750px" center>
|
||||
<div class="dialog-content">
|
||||
<!-- 基础信息 -->
|
||||
<div class="info-group">
|
||||
<h3 class="group-title">基本信息</h3>
|
||||
<div class="field-item">
|
||||
<span class="label">文件名:</span>
|
||||
<span class="value">{{ currentRowData.file_name }}</span>
|
||||
</div>
|
||||
<div class="field-item">
|
||||
<span class="label">采集时间:</span>
|
||||
<span class="value">{{ detailData.datetime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 详细信息 -->
|
||||
<div class="info-group mt-6">
|
||||
<h3 class="group-title">详细信息</h3>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="数据路径">{{
|
||||
detailData.data_path
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="cla视频">
|
||||
<a :href="detailData.reserved_json.bag_player" class="text-blue-500 hover:underline" target="_blank">
|
||||
查看cla视频
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="cla元信息">
|
||||
<a :href="detailData.reserved_json.bag_meta" class="text-blue-500 hover:underline" target="_blank">
|
||||
查看cla元数据
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{
|
||||
currentRowData.comment
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="城区">{{
|
||||
currentRowData.urban ? currentRowData.urban : ""
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="高速">{{
|
||||
currentRowData.highway ? currentRowData.highway : ""
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="parking/driving">{{
|
||||
currentRowData.driving
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="场景时刻">{{
|
||||
currentRowData.front_starttime != null
|
||||
? currentRowData.front_starttime + "-" + currentRowData.front_endtime
|
||||
: null
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="Land_mark标注">{{
|
||||
ldAnnotatedText
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="Object标注">{{
|
||||
odAnnotatedText
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, computed } from "vue";
|
||||
import { ElTree, ElButton, ElMessage } from "element-plus";
|
||||
import type { VxeTableGridOptions } from "#/adapter/vxe-table";
|
||||
import { useVbenVxeGrid } from "#/adapter/vxe-table";
|
||||
import { getRootBagListApi, getSubFstApi } from "#/api/core/rootdata";
|
||||
import { Button } from "ant-design-vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getBagDetailApi } from "#/api/core/baglist";
|
||||
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
|
||||
// 通过 useRoute 获取路由实例
|
||||
const route = useRoute();
|
||||
interface RowType {
|
||||
id: any;
|
||||
file_name: string;
|
||||
capture_datetime: string;
|
||||
bag_status?: number;
|
||||
level1_tag_name?: string;
|
||||
level2_tag_name?: string;
|
||||
level3_tag_name?: string;
|
||||
level4_tag_name?: string;
|
||||
sub_tag_name?: string;
|
||||
status?: string;
|
||||
comment?: string;
|
||||
highway?: string | null;
|
||||
city?: string | null;
|
||||
driving?: string;
|
||||
video_url?: string;
|
||||
front_starttime?: number;
|
||||
front_endtime?: number;
|
||||
}
|
||||
|
||||
const currentRowData = ref<RowType | null>(null);
|
||||
|
||||
const isPlayVideo = ref(false);
|
||||
const currentVideo = ref("");
|
||||
const videoPlayer = ref<HTMLVideoElement | null>(null);
|
||||
|
||||
const isMoreInfo = ref(false);
|
||||
const treeRef = ref();
|
||||
const permissionData = ref([
|
||||
{
|
||||
id: 2,
|
||||
label: "一级标签",
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
label: "二级标签",
|
||||
children: [
|
||||
{ id: 4, label: "三级标签" },
|
||||
{ id: 5, label: "三级标签" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// 使用 ref 包装 gridOptions
|
||||
const gridOptions = reactive<VxeTableGridOptions<RowType>>({
|
||||
// const gridOptions: VxeTableGridOptions<RowType> = {
|
||||
height: "100%",
|
||||
resizable: false, // 禁用列宽调整动画
|
||||
resizeConfig: {},
|
||||
scrollX: {
|
||||
enabled: true,
|
||||
gt: 0, // 禁用虚拟滚动优化
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0, // 禁用虚拟滚动优化
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
useKey: true,
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: "name",
|
||||
},
|
||||
columns: [
|
||||
{ title: "ID", type: "seq", width: 50 },
|
||||
|
||||
{
|
||||
field: "file_name",
|
||||
title: "Bag name",
|
||||
},
|
||||
{
|
||||
field: "level1_tag_name",
|
||||
title: "STS name",
|
||||
},
|
||||
{
|
||||
field: "sub_tag_name",
|
||||
title: "Leaf name",
|
||||
},
|
||||
{
|
||||
field: "comment",
|
||||
title: "描述",
|
||||
},
|
||||
{
|
||||
field: "capture_datetime",
|
||||
title: "采集时间",
|
||||
},
|
||||
{ slots: { default: "action" }, title: "操作", width: 150 },
|
||||
],
|
||||
exportConfig: {},
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
},
|
||||
proxyConfig: {
|
||||
response: {
|
||||
result: "data",
|
||||
total: "total",
|
||||
},
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
// 合并所有参数
|
||||
const queryParams = {
|
||||
page: page.currentPage,
|
||||
per_page: page.pageSize,
|
||||
fst_tag: route.name,
|
||||
filter_fst: treeRef.value.getCheckedKeys(),
|
||||
// ...restFormValues,
|
||||
};
|
||||
|
||||
// console.log("最终发送给后端的参数:", queryParams);
|
||||
const res = await getRootBagListApi(queryParams);
|
||||
return res;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [Grid, tableObject] = useVbenVxeGrid({ gridOptions });
|
||||
// 树形配置
|
||||
const defaultProps = ref({
|
||||
children: "children",
|
||||
label: "label",
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const queryParams = {
|
||||
page: gridOptions.pagerConfig.currentPage || 1,
|
||||
per_page: gridOptions.pagerConfig.pageSize || 20,
|
||||
fst_tag: route.name,
|
||||
filter_fst: treeRef.value?.getCheckedKeys() || [],
|
||||
};
|
||||
|
||||
tableObject.reload(queryParams);
|
||||
} catch (error) {
|
||||
console.error("查询失败:", error);
|
||||
ElMessage.error("查询失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
// 1. 重置树形结构选中状态
|
||||
treeRef.value.setCheckedKeys([]);
|
||||
|
||||
const queryParams = {
|
||||
page: gridOptions.pagerConfig.currentPage || 1,
|
||||
per_page: gridOptions.pagerConfig.pageSize || 20,
|
||||
fst_tag: route.name,
|
||||
filter_fst: treeRef.value?.getCheckedKeys() || [],
|
||||
};
|
||||
|
||||
tableObject.reload(queryParams);
|
||||
ElMessage.info("已重置!");
|
||||
};
|
||||
|
||||
const currentBagName = ref("");
|
||||
const playVideo = async (row: any) => {
|
||||
isPlayVideo.value = true;
|
||||
// console.log(row.file_name);
|
||||
currentVideo.value = row.video_url;
|
||||
currentBagName.value = row.file_name;
|
||||
};
|
||||
|
||||
// const detailData = ref();
|
||||
const handleMoreInfo = async (row: any) => {
|
||||
isMoreInfo.value = true;
|
||||
// console.log(21, row);
|
||||
currentRowData.value = row;
|
||||
try {
|
||||
const res = await getBagDetailApi({ bagname: [row.file_name] });
|
||||
// detailData.value = res.data;
|
||||
// const res = {
|
||||
// 'PL162802_event_all_time_event_20250416-193015_0.bag': {
|
||||
// data_path: "b-perception-e2e-1318950322/mb_raw_rosbag_decode_dirs/PL162802_event_all_time_event_20250416-193015_0.bag.dir",
|
||||
// bag_name: 'PL162802_event_all_time_event_20250416-193015_0.bag',
|
||||
// datetime: 'Wed, 16 Apr 2025 11:30:15 GMT',
|
||||
// update_time: 'Fri, 20 Jun 2025 02:33:28 GMT',
|
||||
// is_active_data: true,
|
||||
// is_decoded: true,
|
||||
// ld_annotated: 1,
|
||||
// od_annotated: 1,
|
||||
// reserved_json: {
|
||||
// bag_meta: 'https://cla-dev.ca4ad.com/cdi/data/61af489c7c497d2af13228c78b5882e4',
|
||||
// bag_player: 'https://mviz-dev.ca4ad.com/player/v4/?bag_md5=61af489c7c497d2af13228c78b5882e4',
|
||||
// },
|
||||
// raw_gps: 'https://b-perception-e2e-1318950322.cos.ap-shanghai-adc.myqcloud.com/',
|
||||
// raw_imu: 'https://b-perception-e2e-1318950322.cos.ap-shanghai-adc.myqcloud.com/',
|
||||
// vehicle_wheel: 'https://b-perception-e2e-1318950322.cos.ap-shanghai-adc.myqcloud.com',
|
||||
// }
|
||||
const values = Object.values(res.data);
|
||||
detailData.value = values[0];
|
||||
} catch (error) {
|
||||
ElMessage({
|
||||
message: `请求出错:${error || "未知错误"}`,
|
||||
type: "error",
|
||||
});
|
||||
console.error("获取 bag 详情失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 对话框关闭时重置视频
|
||||
const handleDialogClosed = () => {
|
||||
currentVideo.value = "";
|
||||
// 停止视频播放并重置
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.pause();
|
||||
videoPlayer.value.src = "";
|
||||
}
|
||||
};
|
||||
|
||||
// 原有代码:detailData 定义
|
||||
const detailData = ref();
|
||||
|
||||
// 新增:处理 ld_annotated 显示文本的计算属性
|
||||
const ldAnnotatedText = computed(() => {
|
||||
// 处理数据未加载的情况(避免报错)
|
||||
if (!detailData.value) return "加载中...";
|
||||
|
||||
// 获取 ld_annotated 数值(默认 0,防止 undefined)
|
||||
const ldValue = detailData.value.ld_annotated ?? 0;
|
||||
|
||||
// 数值与显示文本的映射(清晰易维护)
|
||||
switch (ldValue) {
|
||||
case 0:
|
||||
return "未标注";
|
||||
case 1:
|
||||
return "自动标注已完成";
|
||||
case 2:
|
||||
return "人工标注已完成";
|
||||
case 3:
|
||||
return "自动标注和人工标注已完成";
|
||||
default:
|
||||
// 处理异常值(如后端返回 4、null 等)
|
||||
return "未知标注状态";
|
||||
}
|
||||
});
|
||||
|
||||
// 可选新增:同步处理 od_annotated(逻辑与 ld_annotated 一致)
|
||||
const odAnnotatedText = computed(() => {
|
||||
if (!detailData.value) return "加载中...";
|
||||
const odValue = detailData.value.od_annotated ?? 0;
|
||||
switch (odValue) {
|
||||
case 0:
|
||||
return "未标注";
|
||||
case 1:
|
||||
return "自动标注已完成";
|
||||
case 2:
|
||||
return "人工标注已完成";
|
||||
case 3:
|
||||
return "自动标注和人工标注已完成";
|
||||
default:
|
||||
return "未知标注状态";
|
||||
}
|
||||
});
|
||||
|
||||
const fetchFst = async () => {
|
||||
const response = await getSubFstApi({ fst_id: route.name });
|
||||
// console.log(44, response.data)
|
||||
permissionData.value = response.data;
|
||||
};
|
||||
onMounted(() => {
|
||||
fetchFst();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.el-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.el-col {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
border-radius: 4px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.custom-dialog {
|
||||
max-width: 600px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 100px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.el-descriptions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 修正后的 Vue3 样式穿透写法 */
|
||||
.tree-custom {
|
||||
:deep(.el-tree) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 节点容器 */
|
||||
:deep(.el-tree-node__content) {
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 节点文本 */
|
||||
:deep(.el-tree-node__label) {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* 展开/折叠图标 */
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
font-size: 14px !important;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* 复选框容器 */
|
||||
:deep(.el-checkbox) {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* 复选框标签 */
|
||||
:deep(.el-checkbox__label) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.my-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
117
apps/web-ele/src/views/demos/element/index.vue
Normal file
117
apps/web-ele/src/views/demos/element/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElMessage,
|
||||
ElNotification,
|
||||
ElSegmented,
|
||||
ElSpace,
|
||||
ElTable,
|
||||
} from 'element-plus';
|
||||
|
||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
function info() {
|
||||
ElMessage.info('How many roads must a man walk down');
|
||||
}
|
||||
|
||||
function error() {
|
||||
ElMessage.error({
|
||||
duration: 2500,
|
||||
message: 'Once upon a time you dressed so fine',
|
||||
});
|
||||
}
|
||||
|
||||
function warning() {
|
||||
ElMessage.warning('How many roads must a man walk down');
|
||||
}
|
||||
function success() {
|
||||
ElMessage.success(
|
||||
'Cause you walked hand in hand With another man in my place',
|
||||
);
|
||||
}
|
||||
|
||||
function notify(type: NotificationType) {
|
||||
ElNotification({
|
||||
duration: 2500,
|
||||
message: '说点啥呢',
|
||||
type,
|
||||
});
|
||||
}
|
||||
const tableData = [
|
||||
{ prop1: '1', prop2: 'A' },
|
||||
{ prop1: '2', prop2: 'B' },
|
||||
{ prop1: '3', prop2: 'C' },
|
||||
{ prop1: '4', prop2: 'D' },
|
||||
{ prop1: '5', prop2: 'E' },
|
||||
{ prop1: '6', prop2: 'F' },
|
||||
];
|
||||
|
||||
const segmentedValue = ref('Mon');
|
||||
|
||||
const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Element Plus组件使用演示"
|
||||
>
|
||||
<div class="flex flex-wrap gap-5">
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> 按钮 </template>
|
||||
<ElSpace>
|
||||
<ElButton text>Text</ElButton>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Message </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Notification </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> Segmented </template>
|
||||
<ElSegmented
|
||||
v-model="segmentedValue"
|
||||
:options="segmentedOptions"
|
||||
size="large"
|
||||
/>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> V-Loading </template>
|
||||
<div class="flex size-72 items-center justify-center" v-loading="true">
|
||||
一些演示的内容
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<ElTable :data="tableData" stripe>
|
||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
181
apps/web-ele/src/views/demos/form/basic.vue
Normal file
181
apps/web-ele/src/views/demos/form/basic.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
|
||||
value: v,
|
||||
label: `选项${v}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox1',
|
||||
label: 'Checkbox1',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => {
|
||||
return ['A', 'B', 'C', 'D'].map((v) =>
|
||||
h(ElCheckbox, { label: v, value: v }),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbotton',
|
||||
label: 'CheckBotton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'select',
|
||||
label: 'Select',
|
||||
componentProps: {
|
||||
filterable: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
checkbotton: ['B', 'C'],
|
||||
checkbox1: ['A', 'B'],
|
||||
date: new Date(),
|
||||
select: 'B',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
|
||||
title="表单演示"
|
||||
>
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="flex-auto">基础表单演示</span>
|
||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<Form />
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
211
apps/web-ele/src/views/detailpage/baginfo/CardImage.vue
Normal file
211
apps/web-ele/src/views/detailpage/baginfo/CardImage.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="card-image">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="ci-placeholder ci-loading">
|
||||
加载预览中…
|
||||
</div>
|
||||
|
||||
<!-- 失败 -->
|
||||
<div v-else-if="error" class="ci-placeholder ci-error">
|
||||
预览失败
|
||||
</div>
|
||||
|
||||
<!-- 缩略图 -->
|
||||
<img
|
||||
v-else
|
||||
:src="thumbnail!"
|
||||
:alt="alt || 'video preview'"
|
||||
class="ci-img"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
src: string // 视频 URL
|
||||
alt?: string // img alt
|
||||
frameTime?: number // 截帧时间(秒),不传则自动选
|
||||
width?: number // 目标宽度(px),不传则用视频宽
|
||||
height?: number // 目标高度(px),不传则按比例
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loaded', dataUrl: string): void
|
||||
(e: 'error', err: unknown): void
|
||||
(e: 'metaLoaded', info: { duration: number }): void
|
||||
}>()
|
||||
|
||||
const thumbnail = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
|
||||
let videoEl: HTMLVideoElement | null = null
|
||||
|
||||
const cleanupVideo = () => {
|
||||
if (!videoEl) return
|
||||
videoEl.src = ''
|
||||
videoEl.removeAttribute('src')
|
||||
videoEl.load()
|
||||
videoEl = null
|
||||
}
|
||||
|
||||
// 生成缩略图主流程
|
||||
const generateThumbnail = () => {
|
||||
cleanupVideo()
|
||||
thumbnail.value = null
|
||||
loading.value = true
|
||||
error.value = false
|
||||
|
||||
if (!props.src) {
|
||||
loading.value = false
|
||||
error.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const video = document.createElement('video')
|
||||
videoEl = video
|
||||
|
||||
// 尽量支持跨域截图(如果源没开 CORS,会在 canvas.toDataURL 报错)
|
||||
video.crossOrigin = 'anonymous'
|
||||
video.preload = 'metadata'
|
||||
video.muted = true
|
||||
video.playsInline = true
|
||||
video.src = props.src
|
||||
|
||||
const handleError = (e: any) => {
|
||||
if (loading.value) {
|
||||
error.value = true
|
||||
loading.value = false
|
||||
emit('error', e)
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
// 自动选取一个相对安全的时间点
|
||||
const duration = video.duration || 0;
|
||||
emit('metaLoaded', { duration });
|
||||
|
||||
let t = props.frameTime;
|
||||
if (t == null) {
|
||||
// 30s 左右的视频:取 0.5s;更长的,取 1s 或时长的 10%
|
||||
if (duration <= 10) {
|
||||
t = Math.min(0.5, duration / 3)
|
||||
} else if (duration <= 60) {
|
||||
t = 1
|
||||
} else {
|
||||
t = Math.min(2, duration / 10)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
video.currentTime = t!
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeeked = () => {
|
||||
try {
|
||||
const vw = video.videoWidth || 320
|
||||
const vh = video.videoHeight || 180
|
||||
|
||||
let targetW = props.width || vw
|
||||
let targetH = props.height
|
||||
|
||||
if (!targetH) {
|
||||
// 按原始比例推高度
|
||||
const ratio = vh / vw
|
||||
targetH = Math.round(targetW * ratio)
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = targetW
|
||||
canvas.height = targetH
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
throw new Error('Canvas context is null')
|
||||
}
|
||||
|
||||
ctx.drawImage(video, 0, 0, targetW, targetH)
|
||||
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.85)
|
||||
thumbnail.value = dataUrl
|
||||
loading.value = false
|
||||
emit('loaded', dataUrl)
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
return
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (!video) return
|
||||
video.removeEventListener('error', handleError)
|
||||
video.removeEventListener('loadedmetadata', handleLoadedMetadata)
|
||||
video.removeEventListener('seeked', handleSeeked)
|
||||
}
|
||||
|
||||
video.addEventListener('error', handleError)
|
||||
video.addEventListener('loadedmetadata', handleLoadedMetadata)
|
||||
video.addEventListener('seeked', handleSeeked)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
generateThumbnail()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
generateThumbnail()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupVideo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #020617;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ci-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.ci-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ci-loading {
|
||||
background: radial-gradient(circle at 10% 20%, #111827, #020617);
|
||||
}
|
||||
|
||||
.ci-error {
|
||||
background: #111827;
|
||||
}
|
||||
</style>
|
||||
482
apps/web-ele/src/views/detailpage/baginfo/MultiVideoPlayer.vue
Normal file
482
apps/web-ele/src/views/detailpage/baginfo/MultiVideoPlayer.vue
Normal file
@@ -0,0 +1,482 @@
|
||||
<template>
|
||||
<div class="multi-video-player">
|
||||
<!-- 上半部分:视频占据剩余空间 -->
|
||||
<div class="video-shell">
|
||||
<video
|
||||
ref="videoRef"
|
||||
:src="currentSource?.src"
|
||||
controls
|
||||
controlsList="nodownload"
|
||||
@contextmenu.prevent
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onVideoEnded"
|
||||
class="video-el"
|
||||
></video>
|
||||
</div>
|
||||
<!-- 顶部信息(可按需裁掉) -->
|
||||
<div
|
||||
v-if="showInfoBar"
|
||||
class="mt-1 flex justify-between text-xs text-gray-600 px-1"
|
||||
>
|
||||
<span>段落:{{ currentIndex + 1 }} / {{ sources.length }}</span>
|
||||
<span>进度:{{ formatTime(globalCurrentTime) }} / {{ formatTime(totalDuration) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 总进度条(单段时也可用) -->
|
||||
<input
|
||||
v-if="showProgressBar"
|
||||
type="range"
|
||||
min="0"
|
||||
:max="totalDuration"
|
||||
step="0.1"
|
||||
v-model.number="sliderValue"
|
||||
@change="onSliderChange"
|
||||
class="w-full mt-1"
|
||||
/>
|
||||
|
||||
<!-- 段落缩略图预览 -->
|
||||
<div v-if="showSegments && internalSources.length > 1" class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="(item, idx) in internalSources"
|
||||
:key="item.id || idx"
|
||||
class="segment-thumb"
|
||||
:class="{ active: idx === currentIndex }"
|
||||
@click="jumpToSegment(idx)"
|
||||
>
|
||||
<div class="thumb-img-wrap">
|
||||
<CardImage
|
||||
:src="item.src"
|
||||
@metaLoaded="onItemMetaLoad(idx, $event)"
|
||||
/>
|
||||
<!-- 右上角勾选按钮 -->
|
||||
<button
|
||||
v-if="enableSelect"
|
||||
type="button"
|
||||
class="thumb-check"
|
||||
:class="{
|
||||
checked: isSelected(idx),
|
||||
'is-initiator': isInitiator(idx),}"
|
||||
@click.stop="toggleSelect(idx)"
|
||||
>
|
||||
<span v-if="isSelected(idx)">✔</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="thumb-meta">
|
||||
<div
|
||||
class="thumb-title"
|
||||
:title="item.file_name || `第 ${idx + 1} 段`"
|
||||
>
|
||||
{{ item.file_name || `第 ${idx + 1} 段` }}
|
||||
</div>
|
||||
<div class="thumb-line">
|
||||
<span>{{ item.status || '未知' }}</span>
|
||||
</div>
|
||||
<div class="thumb-time">{{ formatTime(item.duration || 0) }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch} from 'vue'
|
||||
import CardImage from './CardImage.vue'
|
||||
|
||||
interface VideoSource {
|
||||
id?: number | string
|
||||
src: string
|
||||
duration?: number // 秒;可以预先给好,也可以运行时测
|
||||
file_name?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
sources: VideoSource[]
|
||||
showInfoBar?: boolean
|
||||
showProgressBar?: boolean
|
||||
showSegments?: boolean
|
||||
enableSelect?: boolean
|
||||
initiatorName?: string
|
||||
/** 父组件传下来的选中列表(file_name数组) */
|
||||
selectedList?: string[]
|
||||
}>()
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
const currentIndex = ref(0)
|
||||
|
||||
// 如果没传 duration,就在 loadedmetadata 时写入
|
||||
const internalSources = ref<VideoSource[]>([])
|
||||
|
||||
watch(
|
||||
() => props.sources,
|
||||
(newSources) => {
|
||||
const prev = internalSources.value
|
||||
const list = (newSources || []).map(s => {
|
||||
const old = prev.find(o =>
|
||||
(o.id ?? o.file_name) === (s.id ?? s.file_name)
|
||||
)
|
||||
return old ? { ...s, duration: old.duration } : { ...s }
|
||||
})
|
||||
internalSources.value = list
|
||||
|
||||
// 防止当前 index 越界
|
||||
if (currentIndex.value >= list.length) {
|
||||
currentIndex.value = list.length > 0 ? 0 : -1
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true} // 立刻跑一次,把初始值也同步
|
||||
)
|
||||
|
||||
const currentSource = computed(() => internalSources.value[currentIndex.value])
|
||||
|
||||
// 总时长
|
||||
const totalDuration = computed(() =>
|
||||
internalSources.value.reduce((sum, s) => sum + (s.duration || 0), 0)
|
||||
)
|
||||
|
||||
// 当前全局播放秒数
|
||||
const globalCurrentTime = ref(0)
|
||||
|
||||
// 绑定到 range 的值
|
||||
const sliderValue = ref(0)
|
||||
|
||||
watch(globalCurrentTime, (val) => {
|
||||
// 播放时同步更新 slider
|
||||
sliderValue.value = val
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'selectedChange', files: string[]): void
|
||||
}>()
|
||||
|
||||
// 选中的 file_name 列表
|
||||
const selectedFileNames = ref<string[]>([])
|
||||
|
||||
// 工具:同步 props.selectedList + 强制包含 initiator
|
||||
const syncSelectedFromProps = () => {
|
||||
let base: string[] = Array.isArray(props.selectedList)
|
||||
? [...props.selectedList]
|
||||
: []
|
||||
|
||||
// 强制把 initiatorName 放进去
|
||||
if (props.initiatorName && !base.includes(props.initiatorName)) {
|
||||
base.unshift(props.initiatorName)
|
||||
}
|
||||
|
||||
selectedFileNames.value = base
|
||||
}
|
||||
|
||||
// 初始 & 之后 props.selectedList / initiatorName 变化时,同步一次
|
||||
watch(
|
||||
() => [props.selectedList, props.initiatorName],
|
||||
() => {
|
||||
syncSelectedFromProps()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 是否 initiator
|
||||
const isInitiator = (index: number) => {
|
||||
const item = internalSources.value[index]
|
||||
if (!item?.file_name || !props.initiatorName) return false
|
||||
return item.file_name === props.initiatorName
|
||||
}
|
||||
|
||||
// 是否选中了某一段(根据 file_name 判断;initiator 强制为选中)
|
||||
const isSelected = (index: number) => {
|
||||
const item = internalSources.value[index]
|
||||
if (!item?.file_name) return false
|
||||
|
||||
if (isInitiator(index)) {
|
||||
// initiator 一定视为已选中
|
||||
return true
|
||||
}
|
||||
return selectedFileNames.value.includes(item.file_name)
|
||||
}
|
||||
|
||||
// 勾选 / 取消勾选某一段
|
||||
const toggleSelect = (index: number) => {
|
||||
if (!props.enableSelect) return
|
||||
|
||||
const item = internalSources.value[index]
|
||||
if (!item?.file_name) return
|
||||
|
||||
// ⭐ initiator 不允许被取消
|
||||
if (isInitiator(index) && isSelected(index)) {
|
||||
return
|
||||
}
|
||||
|
||||
const arr = [...selectedFileNames.value]
|
||||
const i = arr.indexOf(item.file_name)
|
||||
|
||||
if (i >= 0) {
|
||||
// 已存在 → 取消选中
|
||||
arr.splice(i, 1)
|
||||
} else {
|
||||
// 不存在 → 勾上
|
||||
arr.push(item.file_name)
|
||||
}
|
||||
|
||||
selectedFileNames.value = arr
|
||||
emit('selectedChange', arr) // 通知父组件:当前选中的 file_name 列表
|
||||
}
|
||||
|
||||
const onItemMetaLoad = (index: number, info: { duration: number }) => {
|
||||
// 更新 internalSources 里的 duration
|
||||
if (internalSources.value[index]) {
|
||||
internalSources.value[index].duration = info.duration
|
||||
}
|
||||
}
|
||||
|
||||
// 每次 timeupdate 都需要更新“全局进度”
|
||||
const onTimeUpdate = () => {
|
||||
const video = videoRef.value
|
||||
if (!video) return
|
||||
|
||||
const before = internalSources.value
|
||||
.slice(0, currentIndex.value)
|
||||
.reduce((sum, s) => sum + (s.duration || 0), 0)
|
||||
|
||||
globalCurrentTime.value = before + video.currentTime
|
||||
}
|
||||
|
||||
// 一个视频播完,自动切下一个
|
||||
const onVideoEnded = () => {
|
||||
if (currentIndex.value < internalSources.value.length - 1) {
|
||||
currentIndex.value += 1
|
||||
// 自动播放下一段
|
||||
nextTickPlay()
|
||||
}
|
||||
}
|
||||
|
||||
const nextTickPlay = () => {
|
||||
requestAnimationFrame(() => {
|
||||
const video = videoRef.value
|
||||
if (video) {
|
||||
video.currentTime = 0
|
||||
video.play().catch(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 拖动总进度条后,跳到对应段对应时间
|
||||
const onSliderChange = () => {
|
||||
let target = sliderValue.value
|
||||
if (target < 0) target = 0
|
||||
if (target > totalDuration.value) target = totalDuration.value
|
||||
|
||||
// 找到 target 落在哪一段
|
||||
let acc = 0
|
||||
let segmentIndex = 0
|
||||
for (let i = 0; i < internalSources.value.length; i++) {
|
||||
const d = internalSources.value[i].duration || 0
|
||||
if (target <= acc + d) {
|
||||
segmentIndex = i
|
||||
break
|
||||
}
|
||||
acc += d
|
||||
}
|
||||
|
||||
const offsetInSegment = target - acc
|
||||
currentIndex.value = segmentIndex
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const video = videoRef.value
|
||||
if (video) {
|
||||
video.currentTime = offsetInSegment
|
||||
video.play().catch(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 点击切换到指定段的开头
|
||||
const jumpToSegment = (index: number) => {
|
||||
if (index < 0 || index >= internalSources.value.length) return
|
||||
currentIndex.value = index
|
||||
|
||||
// 计算该段起始时刻对应的全局时间
|
||||
const before = internalSources.value
|
||||
.slice(0, index)
|
||||
.reduce((sum, s) => sum + (s.duration || 0), 0)
|
||||
|
||||
globalCurrentTime.value = before
|
||||
sliderValue.value = before
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const video = videoRef.value
|
||||
if (video) {
|
||||
video.currentTime = 0
|
||||
video.play().catch(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 工具函数:秒 -> mm:ss
|
||||
const formatTime = (sec: number) => {
|
||||
sec = sec || 0
|
||||
const m = Math.floor(sec / 60)
|
||||
const s = Math.floor(sec % 60)
|
||||
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.multi-video-player {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.multi-video-player {
|
||||
width: 100%;
|
||||
height: 100%; /* 吃满父容器高度 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* 防止子元素撑破 */
|
||||
}
|
||||
|
||||
/* 视频容器:占据剩余空间,可被压缩 */
|
||||
.video-shell {
|
||||
position: relative;
|
||||
flex: 1; /* ⭐ 关键:占据剩余空间 */
|
||||
min-height: 0; /* ⭐ 允许被压缩到更小 */
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 视频本体:绝对铺满 video-shell */
|
||||
.video-el {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* 或 contain,看你需求 */
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.segments button {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 每个卡片:固定宽度,竖向布局(上图下文) */
|
||||
.segment-thumb {
|
||||
width: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.segment-thumb:hover {
|
||||
border-color: #93c5fd;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.segments button.active {
|
||||
font-weight: bold;
|
||||
border: 1px solid #409eff;
|
||||
}
|
||||
|
||||
.segment-thumb.active {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* 图片区域:宽度跟卡片一致 + 16:9 比例 */
|
||||
.thumb-img-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9; /* ⭐ 固定 16:9 */
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #020617;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 右上角的勾选按钮 */
|
||||
.thumb-check {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: rgba(15, 23, 42, 0.75);
|
||||
color: #e5e7eb;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 勾上之后的视觉状态 */
|
||||
.thumb-check.checked {
|
||||
border-color: #3b82f6;
|
||||
background: #3b82f6;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* initiator:必选,不允许取消,视觉上稍微弱一点强调 */
|
||||
.thumb-check.is-initiator {
|
||||
cursor: not-allowed;
|
||||
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.8);
|
||||
}
|
||||
.thumb-check.is-initiator.checked {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.thumb-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.thumb-title {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
max-width: 180px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* 限制为两行 */
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 下面几行状态 / 标签 / 时长 */
|
||||
.thumb-line {
|
||||
margin-top: 2px;
|
||||
font-size: 8px;
|
||||
color: #4b5563;
|
||||
}
|
||||
.thumb-time {
|
||||
margin-top: 2px;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
195
apps/web-ele/src/views/detailpage/baginfo/caseConfirm.vue
Normal file
195
apps/web-ele/src/views/detailpage/baginfo/caseConfirm.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<!-- 关键修改:添加容器控制宽度和间距 -->
|
||||
<div class="mx-auto w-full max-w-md mt-8">
|
||||
<div class="h-full rounded-lg border-gray-200 bg-white p-4">
|
||||
<h3 class="mb-6 text-lg font-semibold text-gray-800">场景选择</h3>
|
||||
|
||||
<!-- 筛选表单 -->
|
||||
<form class="space-y-6" @submit.prevent="handleSubmit">
|
||||
<!-- 开始时间 (修改为数字输入) -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">Start time</label>
|
||||
<!-- 使用 input type="number" 替代 el-time-picker -->
|
||||
<!-- min 和 max 属性限制输入范围 -->
|
||||
<!-- step="1" 确保只能输入整数 -->
|
||||
<input v-model.number="formData.startTime" type="number" placeholder="输入0-200的数字" min="0" max="200" step="1"
|
||||
class="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<!-- 结束时间 (修改为数字输入) -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">End time</label>
|
||||
<!-- 使用 input type="number" 替代 el-time-picker -->
|
||||
<!-- min 和 max 属性限制输入范围 -->
|
||||
<!-- step="1" 确保只能输入整数 -->
|
||||
<input v-model.number="formData.endTime" type="number" placeholder="输入0-200的数字" min="0" max="200" step="1"
|
||||
class="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<!-- 是否为复杂案例 -->
|
||||
<!-- <div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">Whether hard case</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
<input type="radio" id="complex" name="caseType" v-model="formData.caseType" :value="2"
|
||||
class="h-4 w-4 text-blue-600" />
|
||||
<label for="complex" class="ml-2 text-gray-700">复杂场景</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="radio" id="simple" name="caseType" v-model="formData.caseType" :value="1"
|
||||
class="h-4 w-4 text-blue-600" />
|
||||
<label for="simple" class="ml-2 text-gray-700">简单场景</label>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 是否为复杂案例 (横向展示,使用 Element Plus 单选框) -->
|
||||
<div class="pt-4">
|
||||
<label class="mb-2 block text font-medium text-gray-700">场景类型</label>
|
||||
<el-radio-group v-model="formData.caseType" class="flex space-x-6">
|
||||
<el-radio :value="2">复杂场景</el-radio>
|
||||
<el-radio :value="1">简单场景</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 场景类型多选框 -->
|
||||
<div>
|
||||
<!-- <label class="mb-2 block text-sm font-medium text-gray-700">场景类型</label> -->
|
||||
<el-checkbox-group v-model="formData.sceneTypes">
|
||||
<div class="flex gap-6 mx-auto">
|
||||
<el-checkbox value="urban">城区</el-checkbox>
|
||||
<el-checkbox value="high_speed">高速</el-checkbox>
|
||||
<el-checkbox value="parking">parking</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<div class="mt-4 flex justify-center">
|
||||
<button type="submit"
|
||||
class="confirm-button rounded-md bg-blue-500 px-6 py-2 font-medium text-white transition-colors duration-200 hover:bg-blue-600">
|
||||
confirm
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
type CaseFormModel = {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
caseType: number;
|
||||
sceneTypes: string[];
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submit-form', payload: CaseFormModel): void;
|
||||
(e: 'update:modelValue', payload: CaseFormModel): void;
|
||||
}>();
|
||||
|
||||
const dateTimeProp = defineProps({
|
||||
formattedTime: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
initialStartTime: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
initialEndTime: {
|
||||
type: Number,
|
||||
default: 16,
|
||||
},
|
||||
initialCaseType: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
initialSceneTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 使用一个对象来管理所有表单字段
|
||||
const formData = ref<CaseFormModel>({
|
||||
startTime: 2, // 修改为数字,并设置默认值
|
||||
endTime: 16, // 修改为数字,并保持默认值
|
||||
caseType: 1, // 默认选择简单场景
|
||||
sceneTypes: [] as string[], // 新增场景类型多选框,默认为空数组
|
||||
});
|
||||
|
||||
watch(
|
||||
() => dateTimeProp.modelValue as CaseFormModel | undefined,
|
||||
(mv) => {
|
||||
if (mv) {
|
||||
formData.value = {
|
||||
startTime: Number(mv.startTime ?? 2),
|
||||
endTime: Number(mv.endTime ?? 16),
|
||||
caseType: Number(mv.caseType ?? 1),
|
||||
sceneTypes: Array.isArray(mv.sceneTypes) ? mv.sceneTypes : [],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
formData.value = {
|
||||
startTime: Number(dateTimeProp.initialStartTime),
|
||||
endTime: Number(dateTimeProp.initialEndTime),
|
||||
caseType: Number(dateTimeProp.initialCaseType),
|
||||
sceneTypes: Array.isArray(dateTimeProp.initialSceneTypes)
|
||||
? (dateTimeProp.initialSceneTypes as string[])
|
||||
: [],
|
||||
};
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
formData,
|
||||
(val) => {
|
||||
emit('update:modelValue', {
|
||||
startTime: Number(val.startTime),
|
||||
endTime: Number(val.endTime),
|
||||
caseType: Number(val.caseType),
|
||||
sceneTypes: Array.isArray(val.sceneTypes) ? val.sceneTypes : [],
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
// 确保值是数字,并在有效范围内
|
||||
const start = Number(formData.value.startTime);
|
||||
const end = Number(formData.value.endTime);
|
||||
|
||||
if (start < 0 || end < 0 ) {
|
||||
// 可以添加一个提示,告诉用户输入无效
|
||||
console.warn('Start time and End time must be between 0 and 200.');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(559, formData.value)
|
||||
emit('submit-form', {
|
||||
...formData.value,
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 移除全局样式,使用scoped样式 */
|
||||
.confirm-button {
|
||||
border-width: 1px;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
</style>
|
||||
238
apps/web-ele/src/views/detailpage/baginfo/comment.vue
Normal file
238
apps/web-ele/src/views/detailpage/baginfo/comment.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<!-- src/views/Comments.vue -->
|
||||
<template>
|
||||
<div class="h-full p-2">
|
||||
<div class="flex h-full flex-col space-y-4">
|
||||
<div class="mb-2 flex items-center text-lg font-semibold">
|
||||
备注区
|
||||
<span class="ml-2 rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-700">
|
||||
{{ comments.length }}/3
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表(可滚动区域) -->
|
||||
<div class="flex-1 space-y-4 overflow-y-auto pr-2">
|
||||
<!-- 无评论提示 -->
|
||||
<div v-if="comments.length === 0" class="rounded-lg bg-gray-50 py-6 text-center text-gray-500">
|
||||
暂无评论
|
||||
</div>
|
||||
|
||||
<!-- 评论项 -->
|
||||
<div v-for="(comment, index) in comments" :key="index" class="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div class="flex">
|
||||
<!-- 用户信息 -->
|
||||
<div class="flex-1">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<!-- <div class="font-medium text-gray-800">
|
||||
{{ comment.author }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">{{ comment.date }}</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="editingIndex === index" class="mb-2">
|
||||
<el-input v-model="editingComment.content" type="textarea" :rows="3" maxlength="500" show-word-limit
|
||||
@keyup.enter="!$event.shiftKey && saveEdit()" />
|
||||
<div class="mt-2 flex justify-end space-x-2">
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
<el-button type="primary" size="small" @click="saveEdit">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示模式 -->
|
||||
<p v-else class="text-gray-700">{{ comment.content }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 编辑按钮(仅在非编辑模式下显示) -->
|
||||
<div v-if="editingIndex !== index" class=" flex">
|
||||
<el-button-group class="flex flex-col">
|
||||
<el-button type="primary" size="small" @click="startEdit(index)" plain class="w-16">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="deleteComment(index)" plain class="w-16">
|
||||
删除
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论输入框 -->
|
||||
<div class="mt-2 rounded-lg border bg-gray-50 p-4">
|
||||
<el-input v-model="newComment.content" type="textarea" :rows="4" :placeholder="comments.length >= 3 ? '评论已达上限(3条)' : '请输入您的评论内容'
|
||||
" maxlength="500" show-word-limit :disabled="comments.length >= 3"
|
||||
@keyup.enter="!$event.shiftKey && submitComment()" />
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<span v-if="comments.length >= 3" class="text-xs text-gray-500">
|
||||
评论数量已达上限,无法继续添加
|
||||
</span>
|
||||
<div class="flex-1"></div>
|
||||
<el-button type="primary" size="small" @click="submitComment"
|
||||
:disabled="!newComment.content || comments.length >= 3">
|
||||
提交评论
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
// 评论数据结构
|
||||
interface Comment {
|
||||
id?: number;
|
||||
author?: string;
|
||||
content?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
initialData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义 emit
|
||||
const emit = defineEmits(['update-comments']);
|
||||
|
||||
// 评论列表
|
||||
const comments = ref<Comment[]>([]);
|
||||
const newComment = reactive({
|
||||
content: '',
|
||||
});
|
||||
|
||||
// 编辑状态
|
||||
const editingIndex = ref<number | null>(null);
|
||||
const editingComment = reactive({
|
||||
content: '',
|
||||
});
|
||||
|
||||
// 开始编辑
|
||||
const startEdit = (index: number) => {
|
||||
editingIndex.value = index;
|
||||
editingComment.content = comments.value[index].content || '';
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = () => {
|
||||
if (editingIndex.value === null) return;
|
||||
|
||||
if (!editingComment.content.trim()) {
|
||||
ElMessage({
|
||||
message: '评论内容不能为空',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新评论内容
|
||||
comments.value[editingIndex.value].content = editingComment.content.trim();
|
||||
|
||||
// 重置编辑状态
|
||||
cancelEdit();
|
||||
|
||||
// 触发事件,将更新后的评论数据传递给父组件
|
||||
emit('update-comments', comments.value);
|
||||
|
||||
ElMessage({
|
||||
message: '评论更新成功!',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
editingIndex.value = null;
|
||||
editingComment.content = '';
|
||||
};
|
||||
|
||||
// 删除评论
|
||||
const deleteComment = (index: number) => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要删除这条评论吗?',
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
comments.value.splice(index, 1);
|
||||
emit('update-comments', comments.value);
|
||||
ElMessage({
|
||||
message: '评论删除成功!',
|
||||
type: 'success',
|
||||
});
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
// 提交评论
|
||||
const submitComment = () => {
|
||||
// 检查评论数量是否已达上限
|
||||
if (comments.value.length >= 3) {
|
||||
ElMessage({
|
||||
message: '评论已达上限(3条)',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newComment.content.trim()) {
|
||||
ElMessage({
|
||||
message: '评论内容不能为空',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加新评论到列表
|
||||
const comment: Comment = {
|
||||
content: newComment.content.trim(),
|
||||
};
|
||||
|
||||
comments.value.push(comment);
|
||||
|
||||
// 重置表单
|
||||
newComment.content = '';
|
||||
|
||||
// 触发事件,将评论数据传递给父组件
|
||||
emit('update-comments', comments.value);
|
||||
|
||||
// 显示成功提示
|
||||
ElMessage({
|
||||
message: '评论提交成功!',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
// 模拟加载已有评论
|
||||
onMounted(async () => {
|
||||
// 在实际应用中,这里应该从API获取评论
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// 清空 comments(安全起见)
|
||||
comments.value = [];
|
||||
|
||||
// 加载初始评论
|
||||
if (props.initialData[0]) {
|
||||
comments.value[0] = { content: props.initialData[0] };
|
||||
}
|
||||
if (props.initialData[1]) {
|
||||
comments.value[1] = { content: props.initialData[1] };
|
||||
}
|
||||
if (props.initialData[2]) {
|
||||
comments.value[2] = { content: props.initialData[2] };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加一些自定义样式 */
|
||||
</style>
|
||||
58
apps/web-ele/src/views/detailpage/baginfo/conformButton.vue
Normal file
58
apps/web-ele/src/views/detailpage/baginfo/conformButton.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div
|
||||
class="mt-0 grid grid-cols-1 gap-4 md:grid-cols-12 md:items-center md:gap-8"
|
||||
>
|
||||
<div class="flex justify-center md:col-span-12">
|
||||
<el-radio-group
|
||||
v-model="selectedValue"
|
||||
@change="handleRadioChange"
|
||||
>
|
||||
<el-radio :label="1">符合描述</el-radio>
|
||||
<el-radio :label="0" class="ml-4">不符合描述</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: number | null;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'switch-change']);
|
||||
|
||||
const normalizeValue = (value: unknown): number | null => {
|
||||
if (value === 0 || value === 1) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 记录是否是第一次访问(当外部值不是 0/1 时视为“未选择”)
|
||||
const isFirstVisit = ref(true);
|
||||
const selectedValue = ref<number | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
selectedValue.value = normalizeValue(val);
|
||||
isFirstVisit.value = selectedValue.value === null;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleRadioChange = (value: number) => {
|
||||
// 首次访问时,更新状态并触发事件
|
||||
if (isFirstVisit.value) {
|
||||
isFirstVisit.value = false;
|
||||
}
|
||||
|
||||
// 无论是否首次,都正常触发事件
|
||||
const next = normalizeValue(value);
|
||||
selectedValue.value = next;
|
||||
emits('update:modelValue', next);
|
||||
emits('switch-change', next);
|
||||
};
|
||||
</script>
|
||||
1301
apps/web-ele/src/views/detailpage/baginfo/index.vue
Normal file
1301
apps/web-ele/src/views/detailpage/baginfo/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1542
apps/web-ele/src/views/detailpage/baginfo/indexupdated.vue
Normal file
1542
apps/web-ele/src/views/detailpage/baginfo/indexupdated.vue
Normal file
File diff suppressed because it is too large
Load Diff
229
apps/web-ele/src/views/detailpage/baginfo/note.vue
Normal file
229
apps/web-ele/src/views/detailpage/baginfo/note.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<!-- src/components/NoteComponent.vue -->
|
||||
<template>
|
||||
<div class="h-full p-2">
|
||||
<div class="flex h-full flex-col space-y-4">
|
||||
<div class="mb-2 flex items-center text-lg font-semibold">
|
||||
备注区
|
||||
<span class="ml-2 rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-700">
|
||||
{{ comments.length }}/3
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表(可滚动区域) -->
|
||||
<div class="flex-1 space-y-4 overflow-y-auto pr-2">
|
||||
<!-- 无评论提示 -->
|
||||
<div v-if="comments.length === 0" class="rounded-lg bg-gray-50 py-6 text-center text-gray-500">
|
||||
暂无备注
|
||||
</div>
|
||||
|
||||
<!-- 评论项 -->
|
||||
<div v-for="(comment, index) in comments" :key="index" class="rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div class="flex">
|
||||
<!-- 用户信息 -->
|
||||
<div class="flex-1">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-500">{{ new Date().toLocaleTimeString() }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="editingIndex === index" class="mb-2">
|
||||
<el-input
|
||||
v-model="editingComment.content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
@keyup.enter="!$event.shiftKey && saveEdit()"
|
||||
/>
|
||||
<div class="mt-2 flex justify-end space-x-2">
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
<el-button type="primary" size="small" @click="saveEdit">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示模式 -->
|
||||
<p v-else class="text-gray-700">{{ comment.content }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 编辑按钮(仅在非编辑模式下显示) -->
|
||||
<div v-if="editingIndex !== index" class="flex">
|
||||
<el-button-group class="flex flex-col">
|
||||
<el-button type="primary" size="small" @click="startEdit(index)" plain class="w-16">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="deleteComment(index)" plain class="w-16">
|
||||
删除
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论输入框 -->
|
||||
<div class="mt-2 rounded-lg border bg-gray-50 p-4">
|
||||
<el-input
|
||||
v-model="newComment.content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="comments.length >= 3 ? '备注已达上限(3条)' : '请输入您的备注内容'"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:disabled="comments.length >= 3"
|
||||
@keyup.enter="!$event.shiftKey && submitComment()"
|
||||
/>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<span v-if="comments.length >= 3" class="text-xs text-gray-500">
|
||||
备注数量已达上限,无法继续添加
|
||||
</span>
|
||||
<div class="flex-1"></div>
|
||||
<el-button type="primary" size="small" @click="submitComment"
|
||||
:disabled="!newComment.content || comments.length >= 3">
|
||||
提交备注
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
initialData: {
|
||||
type: Array as () => string[],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义 emit
|
||||
const emit = defineEmits(['update-comments']);
|
||||
|
||||
// 评论数据结构
|
||||
interface Comment {
|
||||
content: string;
|
||||
}
|
||||
|
||||
// 评论列表
|
||||
const comments = ref<Comment[]>([]);
|
||||
const newComment = reactive({
|
||||
content: '',
|
||||
});
|
||||
|
||||
// 编辑状态
|
||||
const editingIndex = ref<number | null>(null);
|
||||
const editingComment = reactive({
|
||||
content: '',
|
||||
});
|
||||
|
||||
// 开始编辑
|
||||
const startEdit = (index: number) => {
|
||||
editingIndex.value = index;
|
||||
editingComment.content = comments.value[index].content || '';
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = () => {
|
||||
if (editingIndex.value === null) return;
|
||||
|
||||
if (!editingComment.content.trim()) {
|
||||
ElMessage({
|
||||
message: '备注内容不能为空',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新评论内容
|
||||
comments.value[editingIndex.value].content = editingComment.content.trim();
|
||||
|
||||
// 重置编辑状态
|
||||
cancelEdit();
|
||||
|
||||
// 触发事件,将更新后的评论数据传递给父组件
|
||||
emit('update-comments', comments.value);
|
||||
|
||||
ElMessage({
|
||||
message: '备注更新成功!',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
editingIndex.value = null;
|
||||
editingComment.content = '';
|
||||
};
|
||||
|
||||
// 删除评论
|
||||
const deleteComment = (index: number) => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要删除这条备注吗?',
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
comments.value.splice(index, 1);
|
||||
emit('update-comments', comments.value);
|
||||
ElMessage({
|
||||
message: '备注删除成功!',
|
||||
type: 'success',
|
||||
});
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
});
|
||||
};
|
||||
|
||||
// 提交评论
|
||||
const submitComment = () => {
|
||||
// 检查评论数量是否已达上限
|
||||
if (comments.value.length >= 3) {
|
||||
ElMessage({
|
||||
message: '备注已达上限(3条)',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newComment.content.trim()) {
|
||||
ElMessage({
|
||||
message: '备注内容不能为空',
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加新评论到列表
|
||||
const comment: Comment = {
|
||||
content: newComment.content.trim(),
|
||||
};
|
||||
|
||||
comments.value.push(comment);
|
||||
|
||||
// 重置表单
|
||||
newComment.content = '';
|
||||
|
||||
// 触发事件,将评论数据传递给父组件
|
||||
emit('update-comments', comments.value);
|
||||
|
||||
// 显示成功提示
|
||||
ElMessage({
|
||||
message: '备注提交成功!',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
// 加载初始数据
|
||||
onMounted(() => {
|
||||
// 将初始数据转换为评论格式
|
||||
comments.value = props.initialData
|
||||
.filter(content => content) // 过滤掉空值
|
||||
.map(content => ({ content: content || '' }))
|
||||
});
|
||||
</script>
|
||||
134
apps/web-ele/src/views/detailpage/baginfo/otherSts.vue
Normal file
134
apps/web-ele/src/views/detailpage/baginfo/otherSts.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="mx-auto w-full max-w-md pl-2 pr-2 mt-16 pt-12">
|
||||
<!-- 取消注释并精简,移除了可能产生边框的样式 -->
|
||||
<div class="dialog-card h-full rounded-xl bg-white p-6 ">
|
||||
<!-- 标题 -->
|
||||
<h2 class="mb-2 text-center text-lg font-semibold text-gray-800">
|
||||
Bag 验证
|
||||
</h2>
|
||||
|
||||
<!-- 主问题 -->
|
||||
<p class="mb-8 text-center font-medium tracking-wide text-gray-800">
|
||||
该数据包是否符合另一个 STS 的描述?
|
||||
</p>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<div class="flex flex-col space-y-4">
|
||||
<button @click="handleResponse('yes')" class="custom-button">
|
||||
Yes
|
||||
</button>
|
||||
<button @click="selectInvalid" class="custom-button">No</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="displayInvalid" title="提示" width="500" center>
|
||||
<span>
|
||||
确定该数据不满足任何标签,是无效的吗?
|
||||
</span>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="displayInvalid = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="submitForm">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { selectNoApi } from '#/api/core/baglist';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const emits = defineEmits(['select-result']);
|
||||
const displayInvalid = ref(false);
|
||||
|
||||
const bagId = defineProps({
|
||||
data: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const selctNoParam = ref({
|
||||
bag_id: bagId.data,
|
||||
bag_status: 1,
|
||||
status: 'REVIEWED_BUT_INVALID',
|
||||
source: 'RULE',
|
||||
});
|
||||
|
||||
|
||||
const otherStsResult = ref({
|
||||
parentValue: 0,
|
||||
});
|
||||
|
||||
const handleResponse = (answer: string) => {
|
||||
|
||||
// otherStsResult.value.code = 2;
|
||||
otherStsResult.value.parentValue = 3;
|
||||
// otherStsResult.value.otherLabel = 2
|
||||
emits('select-result', otherStsResult); // code=2,代表有效
|
||||
|
||||
};
|
||||
|
||||
const selectInvalid = async () => displayInvalid.value = true
|
||||
|
||||
async function submitForm() {
|
||||
try {
|
||||
await selectNoApi(selctNoParam.value);
|
||||
ElMessage({
|
||||
message: '提交成功!',
|
||||
type: 'success',
|
||||
});
|
||||
router.push({
|
||||
name: 'Datalabel'
|
||||
});
|
||||
} catch (error) {
|
||||
ElMessage({
|
||||
message: `提交失败!${error}`,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
displayInvalid.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 0.675rem 1rem;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: #1f2937;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
|
||||
/* 移除按钮的边框 */
|
||||
border: 1px solid #111827;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-button:hover {
|
||||
background-color: #f3f4f6;
|
||||
box-shadow: 0 4px 6px rgb(0 0 0 / 15%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.custom-button:active {
|
||||
background-color: #e5e7eb;
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 20%);
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
373
apps/web-ele/src/views/detailpage/baginfo/selectOtherTag.vue
Normal file
373
apps/web-ele/src/views/detailpage/baginfo/selectOtherTag.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<div class="mx-auto max-w-4xl sm:px-0 w-full h-full flex flex-col min-h-0">
|
||||
<!-- 步骤指示器 -->
|
||||
<div class="shrink-0 flex items-center justify-between px-4 border-b border-gray-200 bg-white py-2">
|
||||
<div v-for="(step, index) in steps" :key="index" class="flex-1 text-center py-1.5" :class="[
|
||||
index < currentStep ? 'bg-blue-50 text-blue-700 border border-blue-200 rounded-t-lg' : '',
|
||||
index === currentStep ? 'bg-blue-100 text-blue-800 font-medium border border-blue-300 rounded-t-lg' : '',
|
||||
index > currentStep ? 'bg-gray-50 text-gray-500 border border-gray-200 rounded-t-lg' : ''
|
||||
]">
|
||||
{{ step.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="flex-1 min-h-0 bg-white rounded-b-lg shadow-md overflow-hidden border border-gray-50 w-full flex flex-col">
|
||||
<!-- 标签选择器 -->
|
||||
<div class="shrink-0 p-3 border-b border-gray-100">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ steps[currentStep].label }}选择
|
||||
</label>
|
||||
<el-select v-model="selectedTagId" :placeholder="`请选择${steps[currentStep].label}`" class="w-full" size=""
|
||||
@change="handleTagChange">
|
||||
<el-option v-for="option in currentSelectOptions" :key="option.id" :label="option.name" :value="option.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 标签注释信息 -->
|
||||
<div class="flex-1 min-h-0 p-3 bg-gray-0 flex flex-col">
|
||||
<div class="shrink-0 flex items-center justify-between mb-1">
|
||||
<h3 class="text-sm font-semibold text-gray-800">
|
||||
{{ steps[currentStep].label }}注释信息
|
||||
</h3>
|
||||
<span class="text-xs text-gray-500">
|
||||
共 {{ currentTagDetails.length }} 条
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 滚动容器 -->
|
||||
<div class="flex-1 min-h-0 rounded-lg border border-gray-200 bg-white p-2 overflow-y-auto">
|
||||
<div class="space-y-4">
|
||||
<!-- 展示当前级别所有标签的注释 -->
|
||||
<div v-for="(item, index) in currentTagDetails" :key="index"
|
||||
class="p-4 rounded-lg border transition-all duration-200 hover:shadow-sm"
|
||||
:class="selectedTagId === item.id ? 'border-blue-300 bg-blue-50' : 'border-gray-200'">
|
||||
<div class="flex items-start">
|
||||
<!-- 序号 -->
|
||||
<div
|
||||
class="flex-shrink-0 w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center text-xs text-gray-600 mr-3 mt-0.5">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate">
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<el-checkbox v-model="item.checked" size="small"
|
||||
:disabled="selectedTagId !== null && selectedTagId !== item.id"
|
||||
@change="() => handleCheckChange(item.id)"></el-checkbox>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600 leading-relaxed">
|
||||
{{ item.description || '无注释信息' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="currentTagDetails.length === 0"
|
||||
class="flex flex-col items-center justify-center h-32 text-gray-500">
|
||||
<el-icon class="text-2xl mb-2">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
<p>当前级别暂无标签数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="shrink-0 p-2 bg-white border-t border-gray-100 flex justify-between">
|
||||
<button v-if="currentStep > 0" @click="prevStep"
|
||||
class="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
<el-icon class="mr-2">
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
上一步
|
||||
</button>
|
||||
|
||||
<button v-if="currentStep < steps.length - 1" @click="nextStep" :disabled="selectedTagId === null"
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors" :class="selectedTagId === null
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'">
|
||||
下一步
|
||||
<el-icon class="ml-2">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</button>
|
||||
|
||||
<button v-else @click="submitSelection" :disabled="!isSelectionComplete"
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors" :class="!isSelectionComplete
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-green-600 text-white hover:bg-green-700'">
|
||||
完成选择
|
||||
<el-icon class="ml-2">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="centerDialogVisible" title="Warning" width="500" center>
|
||||
<span>
|
||||
请注意,内容默认不会居中对齐
|
||||
</span>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="centerDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="centerDialogVisible = false">
|
||||
确认
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ArrowLeft, ArrowRight, Check, InfoFilled } from '@element-plus/icons-vue';
|
||||
|
||||
// 定义组件 props - 现在接收all_tags数组
|
||||
const props = defineProps({
|
||||
allTags: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const centerDialogVisible = ref(false)
|
||||
|
||||
// 标签选择完成时的状态
|
||||
const tagSelectFinish = ref(false)
|
||||
// 定义组件 emits
|
||||
const emit = defineEmits(['selection-other-complete'])
|
||||
|
||||
// 状态管理
|
||||
const currentStep = ref(0)
|
||||
const selectedTagId = ref<number | null>(null)
|
||||
const selectionHistory = ref([])
|
||||
// 保存当前选中的一级标签索引,用于加载对应的子标签
|
||||
const selectedLevel1Index = ref<number | null>(null)
|
||||
|
||||
// 步骤配置
|
||||
const steps = ref([
|
||||
{ label: '一级标签', level: 1 },
|
||||
{ label: '二级标签', level: 2 },
|
||||
{ label: '三级标签', level: 3 },
|
||||
{ label: '四级标签', level: 4 }
|
||||
])
|
||||
|
||||
// 替换原有的isSelectionComplete计算属性
|
||||
const isSelectionComplete = computed(() => {
|
||||
// 最后一步且无选项时,只要历史记录不为空即可提交
|
||||
if (currentStep.value === steps.value.length - 1 && currentSelectOptions.value.length === 0) {
|
||||
return selectionHistory.value.length > 0;
|
||||
}
|
||||
// 其他情况:需要选中值且有历史记录
|
||||
return selectedTagId.value !== null && selectionHistory.value.length > 0;
|
||||
});
|
||||
|
||||
// 当前步骤的选择选项(修正四级标签逻辑)
|
||||
const currentSelectOptions = computed(() => {
|
||||
// 一级标签选项:从allTags中提取所有一级标签
|
||||
if (currentStep.value === 0) {
|
||||
return props.allTags.map(item => item.level1_tag);
|
||||
}
|
||||
// 二级标签选项:基于选中的一级标签
|
||||
else if (currentStep.value === 1) {
|
||||
if (selectedLevel1Index.value !== null && props.allTags[selectedLevel1Index.value]) {
|
||||
return props.allTags[selectedLevel1Index.value].level2_tags || [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
// 三级标签选项:基于选中的二级标签
|
||||
else if (currentStep.value === 2) {
|
||||
if (selectedLevel1Index.value !== null && props.allTags[selectedLevel1Index.value]) {
|
||||
const parentId = selectionHistory.value.length > 1
|
||||
? selectionHistory.value[1].id
|
||||
: null;
|
||||
// 修复:三级无有效父ID时返回空
|
||||
return parentId
|
||||
? (props.allTags[selectedLevel1Index.value].level3_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: []; // 原逻辑可能返回所有三级标签,改为空
|
||||
}
|
||||
return [];
|
||||
}
|
||||
// 四级标签选项:基于选中的三级标签(核心修复)
|
||||
else if (currentStep.value === 3) {
|
||||
if (selectedLevel1Index.value !== null && props.allTags[selectedLevel1Index.value]) {
|
||||
const parentId = selectionHistory.value.length > 2
|
||||
? selectionHistory.value[2].id
|
||||
: null;
|
||||
// 修复:parentId为空时返回空数组,而非所有四级标签
|
||||
return parentId
|
||||
? (props.allTags[selectedLevel1Index.value].level4_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: []; // 关键修改:parentId无效时返回空
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
// 当前级别所有标签的注释信息(同步修正四级逻辑)
|
||||
const currentTagDetails = computed(() => {
|
||||
let currentLevelTags = [];
|
||||
|
||||
if (currentStep.value === 0) {
|
||||
currentLevelTags = props.allTags.map(item => item.level1_tag);
|
||||
} else if (currentStep.value === 1) {
|
||||
if (selectedLevel1Index.value !== null && props.allTags[selectedLevel1Index.value]) {
|
||||
currentLevelTags = props.allTags[selectedLevel1Index.value].level2_tags || [];
|
||||
}
|
||||
} else if (currentStep.value === 2) {
|
||||
if (selectedLevel1Index.value !== null && props.allTags[selectedLevel1Index.value]) {
|
||||
const parentId = selectionHistory.value.length > 1 ? selectionHistory.value[1].id : null;
|
||||
// 修复:三级无有效父ID时返回空
|
||||
currentLevelTags = parentId
|
||||
? (props.allTags[selectedLevel1Index.value].level3_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: []; // 原逻辑可能返回所有三级标签,改为空
|
||||
}
|
||||
} else if (currentStep.value === 3) {
|
||||
if (selectedLevel1Index.value !== null && props.allTags[selectedLevel1Index.value]) {
|
||||
const parentId = selectionHistory.value.length > 2 ? selectionHistory.value[2].id : null;
|
||||
// 修复:四级无有效父ID时返回空
|
||||
currentLevelTags = parentId
|
||||
? (props.allTags[selectedLevel1Index.value].level4_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: []; // 关键修改:parentId无效时返回空
|
||||
}
|
||||
}
|
||||
|
||||
return currentLevelTags.map(tag => ({
|
||||
id: tag.id,
|
||||
title: tag.name,
|
||||
description: tag.annotation || '无注释信息',
|
||||
checked: selectedTagId.value === tag.id
|
||||
}));
|
||||
});
|
||||
|
||||
// 组件挂载时初始化 - 不再自动选择一级标签
|
||||
onMounted(() => {
|
||||
// 清空自动选择逻辑,让用户手动选择一级标签
|
||||
selectedTagId.value = null;
|
||||
selectionHistory.value = [];
|
||||
selectedLevel1Index.value = null;
|
||||
})
|
||||
|
||||
// 处理标签选择变化
|
||||
const handleTagChange = (value: number) => {
|
||||
const selectedTag = currentSelectOptions.value.find(tag => tag.id === value);
|
||||
if (selectedTag) {
|
||||
// 创建新的选择历史数组
|
||||
const newHistory = [...selectionHistory.value.slice(0, currentStep.value)];
|
||||
newHistory.push({
|
||||
id: selectedTag.id,
|
||||
name: selectedTag.name,
|
||||
level: currentStep.value + 1
|
||||
});
|
||||
selectionHistory.value = newHistory;
|
||||
|
||||
// 如果是一级标签,记录其索引
|
||||
if (currentStep.value === 0) {
|
||||
const level1Index = props.allTags.findIndex(item => item.level1_tag.id === value);
|
||||
selectedLevel1Index.value = level1Index !== -1 ? level1Index : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理复选框选择
|
||||
const handleCheckChange = (id: number) => {
|
||||
// 单选逻辑:只能选中一个
|
||||
selectedTagId.value = id;
|
||||
handleTagChange(id);
|
||||
}
|
||||
|
||||
// 下一步
|
||||
const nextStep = () => {
|
||||
if (selectedTagId.value === null) return;
|
||||
|
||||
if (currentStep.value < steps.value.length - 1) {
|
||||
currentStep.value++;
|
||||
|
||||
// 自动选择逻辑:如果下一步有选项,才处理选择
|
||||
if (currentSelectOptions.value.length > 0) {
|
||||
if (currentSelectOptions.value.length === 1) {
|
||||
selectedTagId.value = currentSelectOptions.value[0].id;
|
||||
handleTagChange(selectedTagId.value);
|
||||
} else {
|
||||
selectedTagId.value = null;
|
||||
}
|
||||
} else {
|
||||
// 下一步无选项时,补充历史记录
|
||||
const lastStep = steps.value[currentStep.value];
|
||||
selectionHistory.value.push({
|
||||
id: null,
|
||||
name: `无${lastStep.label}数据`,
|
||||
level: lastStep.level
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 上一步
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--;
|
||||
|
||||
// 恢复上一步的选择
|
||||
if (selectionHistory.value.length > currentStep.value) {
|
||||
selectedTagId.value = selectionHistory.value[currentStep.value].id;
|
||||
|
||||
// 如果回到一级标签,恢复对应的索引
|
||||
if (currentStep.value === 0) {
|
||||
const level1Index = props.allTags.findIndex(item => item.level1_tag.id === selectedTagId.value);
|
||||
selectedLevel1Index.value = level1Index !== -1 ? level1Index : null;
|
||||
}
|
||||
} else {
|
||||
selectedTagId.value = null;
|
||||
|
||||
// 如果回到一级标签,清空索引
|
||||
if (currentStep.value === 0) {
|
||||
selectedLevel1Index.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交选择
|
||||
const submitSelection = () => {
|
||||
if (isSelectionComplete.value) {
|
||||
// console.log('提交选择', selectionHistory.value);
|
||||
// 触发完成事件
|
||||
emit('selection-other-complete', {
|
||||
tagSelectFinish: !tagSelectFinish.value,
|
||||
selectedTags: selectionHistory.value,
|
||||
finalTag: selectionHistory.value[selectionHistory.value.length - 1]
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
393
apps/web-ele/src/views/detailpage/baginfo/selectTag.vue
Normal file
393
apps/web-ele/src/views/detailpage/baginfo/selectTag.vue
Normal file
@@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<!-- <div class=""> -->
|
||||
<div class="mx-auto max-w-4xl sm:px-0 w-full h-full flex flex-col min-h-0"> <!-- 占满父div宽度 -->
|
||||
<!-- 标题区域 -->
|
||||
|
||||
<!-- 步骤指示器(文字缩小) -->
|
||||
<div class="shrink-0 flex items-center justify-between px-4 border-b border-gray-200 bg-white py-2"> <!-- 文字大小改为text-xs -->
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="flex-1 text-center py-1.5"
|
||||
:class="[
|
||||
index < currentStep ? 'bg-blue-50 text-blue-700 border border-blue-200 rounded-t-lg' : '',
|
||||
index === currentStep ? 'bg-blue-100 text-blue-800 font-medium border border-blue-300 rounded-t-lg' : '',
|
||||
index > currentStep ? 'bg-gray-50 text-gray-500 border border-gray-200 rounded-t-lg' : ''
|
||||
]"
|
||||
>
|
||||
{{ step.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区(占满父容器) -->
|
||||
<div class="flex-1 min-h-0 bg-white rounded-b-lg shadow-md overflow-hidden border border-gray-50 w-full flex flex-col">
|
||||
<!-- 标签选择器 -->
|
||||
<div class="shrink-0 p-3 border-b border-gray-100">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ steps[currentStep].label }}选择
|
||||
</label>
|
||||
<el-select
|
||||
v-model="selectedTagId"
|
||||
:placeholder="`请选择${steps[currentStep].label}`"
|
||||
class="w-full"
|
||||
size=""
|
||||
@change="handleTagChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in currentSelectOptions"
|
||||
:key="option.id"
|
||||
:label="option.name"
|
||||
:value="option.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 标签注释信息(降低高度) -->
|
||||
<div class="flex-1 min-h-0 p-3 bg-gray-0 flex flex-col">
|
||||
<div class="shrink-0 flex items-center justify-between mb-1"> <!-- 减少标题底部间距 -->
|
||||
<h3 class="text-sm font-semibold text-gray-800">
|
||||
{{ steps[currentStep].label }}注释信息
|
||||
</h3>
|
||||
<span class="text-xs text-gray-500">
|
||||
共 {{ currentTagDetails.length }} 条
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 滚动容器(高度降低为200px,可按需调整) -->
|
||||
<div class="flex-1 min-h-0 rounded-lg border border-gray-200 bg-white p-2 overflow-y-auto">
|
||||
<div class="space-y-4">
|
||||
<!-- 展示当前级别所有标签的注释 -->
|
||||
<div
|
||||
v-for="(item, index) in currentTagDetails"
|
||||
:key="index"
|
||||
class="p-4 rounded-lg border transition-all duration-200 hover:shadow-sm"
|
||||
:class="selectedTagId === item.id ? 'border-blue-300 bg-blue-50' : 'border-gray-200'"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<!-- 序号 -->
|
||||
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center text-xs text-gray-600 mr-3 mt-0.5">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate">
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<el-checkbox
|
||||
v-model="item.checked"
|
||||
size="small"
|
||||
:disabled="selectedTagId !== null && selectedTagId !== item.id"
|
||||
@change="() => handleCheckChange(item.id)"
|
||||
></el-checkbox>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600 leading-relaxed">
|
||||
{{ item.description || '无注释信息' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="currentTagDetails.length === 0" class="flex flex-col items-center justify-center h-32 text-gray-500"> <!-- 降低空状态高度 -->
|
||||
<el-icon class="text-2xl mb-2"><InfoFilled /></el-icon>
|
||||
<p>当前级别暂无标签数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="shrink-0 p-2 bg-white border-t border-gray-100 flex justify-between">
|
||||
<button
|
||||
v-if="currentStep > 0"
|
||||
@click="prevStep"
|
||||
class="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<el-icon class="mr-2"><ArrowLeft /></el-icon>
|
||||
上一步
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="currentStep < steps.length - 1"
|
||||
@click="nextStep"
|
||||
:disabled="selectedTagId === null"
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||
:class="selectedTagId === null
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'"
|
||||
>
|
||||
下一步
|
||||
<el-icon class="ml-2"><ArrowRight /></el-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else
|
||||
@click="submitSelection"
|
||||
:disabled="!isSelectionComplete"
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||
:class="!isSelectionComplete
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-green-600 text-white hover:bg-green-700'"
|
||||
>
|
||||
完成选择
|
||||
<el-icon class="ml-2"><Check /></el-icon>
|
||||
<!-- <div v-if="currentStep === steps.length - 1">
|
||||
{{ currentSelectOptions }} 查看是否有选项数据
|
||||
</div> -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="centerDialogVisible" title="Warning" width="500" center>
|
||||
<span>
|
||||
It should be noted that the content will not be aligned in center by
|
||||
default
|
||||
</span>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="centerDialogVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="centerDialogVisible = false">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ArrowLeft, ArrowRight, Check, InfoFilled } from '@element-plus/icons-vue';
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
initialData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
initialSelectedTags: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const centerDialogVisible = ref(false)
|
||||
|
||||
// 标签选择完成时的状态
|
||||
const tagSelectFinish = ref(false)
|
||||
// 定义组件 emits
|
||||
const emit = defineEmits(['selection-complete'])
|
||||
|
||||
// 状态管理
|
||||
const currentStep = ref(0)
|
||||
const selectedTagId = ref<number | null>(null)
|
||||
const selectionHistory = ref([])
|
||||
|
||||
// 步骤配置
|
||||
const steps = ref([
|
||||
{ label: '一级标签', level: 1 },
|
||||
{ label: '二级标签', level: 2 },
|
||||
{ label: '三级标签', level: 3 },
|
||||
{ label: '四级标签', level: 4 }
|
||||
])
|
||||
|
||||
// 替换原有的isSelectionComplete计算属性
|
||||
const isSelectionComplete = computed(() => {
|
||||
// 最后一步且无选项时,只要历史记录不为空即可提交
|
||||
if (currentStep.value === steps.value.length - 1 && currentSelectOptions.value.length === 0) {
|
||||
return selectionHistory.value.length > 0;
|
||||
}
|
||||
// 其他情况:需要选中值且有历史记录
|
||||
return selectedTagId.value !== null && selectionHistory.value.length > 0;
|
||||
});
|
||||
|
||||
// 当前步骤的选择选项(修正四级标签逻辑)
|
||||
const currentSelectOptions = computed(() => {
|
||||
if (currentStep.value === 0) {
|
||||
return props.initialData.level1_tag ? [props.initialData.level1_tag] : []
|
||||
} else if (currentStep.value === 1) {
|
||||
return props.initialData.level2_tags || []
|
||||
} else if (currentStep.value === 2) {
|
||||
// 三级标签过滤逻辑(保持不变)
|
||||
const parentId = selectionHistory.value.length > 1
|
||||
? selectionHistory.value[1].id
|
||||
: null
|
||||
return parentId
|
||||
? (props.initialData.level3_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: [] // 三级无父级时返回空(原逻辑可能返回所有,这里同步优化)
|
||||
} else if (currentStep.value === 3) {
|
||||
// 四级标签过滤逻辑(核心修复)
|
||||
const parentId = selectionHistory.value.length > 2
|
||||
? selectionHistory.value[2].id
|
||||
: null
|
||||
// 修复:parentId 为空时返回空数组,而非所有四级标签
|
||||
return parentId
|
||||
? (props.initialData.level4_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// 当前级别所有标签的注释信息(同步修正四级逻辑)
|
||||
const currentTagDetails = computed(() => {
|
||||
let currentLevelTags = []
|
||||
|
||||
if (currentStep.value === 0) {
|
||||
currentLevelTags = props.initialData.level1_tag ? [props.initialData.level1_tag] : []
|
||||
} else if (currentStep.value === 1) {
|
||||
currentLevelTags = props.initialData.level2_tags || []
|
||||
} else if (currentStep.value === 2) {
|
||||
// 三级标签注释逻辑(保持不变)
|
||||
const parentId = selectionHistory.value.length > 1 ? selectionHistory.value[1].id : null
|
||||
currentLevelTags = parentId
|
||||
? (props.initialData.level3_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: [] // 同步优化:无父级时返回空
|
||||
} else if (currentStep.value === 3) {
|
||||
// 四级标签注释逻辑(核心修复)
|
||||
const parentId = selectionHistory.value.length > 2 ? selectionHistory.value[2].id : null
|
||||
// 修复:parentId 为空时返回空数组
|
||||
currentLevelTags = parentId
|
||||
? (props.initialData.level4_tags || []).filter(tag => tag.parent_id === parentId)
|
||||
: []
|
||||
}
|
||||
|
||||
return currentLevelTags.map(tag => ({
|
||||
id: tag.id,
|
||||
title: tag.name,
|
||||
description: tag.annotation || '无注释信息',
|
||||
checked: selectedTagId.value === tag.id
|
||||
}))
|
||||
})
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
const initial = Array.isArray(props.initialSelectedTags) ? props.initialSelectedTags : [];
|
||||
if (initial.length) {
|
||||
selectionHistory.value = initial.map((t: any) => ({
|
||||
id: t?.id ?? null,
|
||||
name: t?.name ?? '',
|
||||
level: Number(t?.level ?? 0),
|
||||
})).filter((t: any) => t.level >= 1 && t.level <= 4);
|
||||
|
||||
const last = selectionHistory.value[selectionHistory.value.length - 1];
|
||||
if (last) {
|
||||
currentStep.value = Math.min(Math.max(last.level - 1, 0), steps.value.length - 1);
|
||||
selectedTagId.value = last.id ?? null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有一级标签,自动选择它作为第一步
|
||||
if (props.initialData.level1_tag) {
|
||||
selectedTagId.value = props.initialData.level1_tag.id
|
||||
selectionHistory.value = [{
|
||||
id: props.initialData.level1_tag.id,
|
||||
name: props.initialData.level1_tag.name,
|
||||
level: 1
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// 处理标签选择变化
|
||||
const handleTagChange = (value: number) => {
|
||||
const selectedTag = currentSelectOptions.value.find(tag => tag.id === value);
|
||||
if (selectedTag) {
|
||||
// 创建新的选择历史数组,而不是截取现有数组
|
||||
const newHistory = [...selectionHistory.value.slice(0, currentStep.value)];
|
||||
newHistory.push({
|
||||
id: selectedTag.id,
|
||||
name: selectedTag.name,
|
||||
level: currentStep.value + 1
|
||||
});
|
||||
selectionHistory.value = newHistory;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理复选框选择
|
||||
const handleCheckChange = (id: number) => {
|
||||
// 单选逻辑:只能选中一个
|
||||
selectedTagId.value = id
|
||||
handleTagChange(id)
|
||||
}
|
||||
|
||||
// 下一步
|
||||
const nextStep = () => {
|
||||
if (selectedTagId.value === null) return;
|
||||
|
||||
if (currentStep.value < steps.value.length - 1) {
|
||||
currentStep.value++;
|
||||
|
||||
// 自动选择逻辑:如果下一步有选项,才处理选择
|
||||
if (currentSelectOptions.value.length > 0) {
|
||||
if (currentSelectOptions.value.length === 1) {
|
||||
selectedTagId.value = currentSelectOptions.value[0].id;
|
||||
handleTagChange(selectedTagId.value);
|
||||
} else {
|
||||
selectedTagId.value = null;
|
||||
}
|
||||
} else {
|
||||
// 下一步无选项时,保留上一步的选择记录(不重置selectedTagId)
|
||||
// 手动补充最后一步的历史记录
|
||||
const lastStep = steps.value[currentStep.value];
|
||||
selectionHistory.value.push({
|
||||
id: null,
|
||||
name: `无${lastStep.label}数据`,
|
||||
level: lastStep.level
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 上一步
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--
|
||||
|
||||
// 恢复上一步的选择
|
||||
if (selectionHistory.value.length > currentStep.value) {
|
||||
selectedTagId.value = selectionHistory.value[currentStep.value].id
|
||||
} else {
|
||||
selectedTagId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交选择
|
||||
const submitSelection = () => {
|
||||
if (selectedTagId.value !== null && selectionHistory.value.length > 0) {
|
||||
// 展示弹框
|
||||
// centerDialogVisible.value=true
|
||||
console.log('提交选择', selectionHistory.value);
|
||||
// 触发完成事件
|
||||
emit('selection-complete', {
|
||||
tagSelectFinish:!tagSelectFinish.value,
|
||||
selectedTags: selectionHistory.value,
|
||||
finalTag: selectionHistory.value[selectionHistory.value.length - 1]
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
278
apps/web-ele/src/views/detailpage/baginfo/summary.vue
Normal file
278
apps/web-ele/src/views/detailpage/baginfo/summary.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">BAG 文件详情</h2>
|
||||
</div>
|
||||
<div class="info-scroll-container h-[55vh]">
|
||||
<el-descriptions :column="1" border size="large" :title="null" class="custom-descriptions" :span="16">
|
||||
<!-- 基础信息项 -->
|
||||
<el-descriptions-item label="文件名">
|
||||
<el-tag type="primary">{{ data.bag_name }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="采集时间">
|
||||
<el-tag type="success">{{ data.datetime }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
<el-tag type="success">{{ data.update_time }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="data.is_active_data ? 'success' : 'danger'">活跃数据</el-tag>
|
||||
<el-tag :type="data.is_decoded ? 'success' : 'warning'" class="ml-2">已解码</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- 优化后的链接项(使用HTML实体和CSS图标) -->
|
||||
<el-descriptions-item label="CLA视频">
|
||||
<a :href="data.reserved_json.bag_player" target="_blank" class="custom-link video-link">
|
||||
<span class="link-icon">▶</span>
|
||||
<span class="link-text">在 MViz 中查看视频</span>
|
||||
<span class="external-icon">↗</span>
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="CLA元数据">
|
||||
<a :href="data.reserved_json.bag_meta" target="_blank" class="custom-link meta-link">
|
||||
<span class="link-icon">📄</span>
|
||||
<span class="link-text">查看 Bag Meta 信息</span>
|
||||
<span class="external-icon">↗</span>
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="Land_mark标注">
|
||||
<el-tag type="primary" class="annotation-tag">
|
||||
<span class="tag-icon">📍</span>
|
||||
{{ getAnnotationText(data.ld_annotated) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="Object标注">
|
||||
<el-tag type="primary" class="annotation-tag">
|
||||
<span class="tag-icon">📦</span>
|
||||
{{ getAnnotationText(data.od_annotated) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="Ego_motion数据">
|
||||
<a :href="data.reserved_json.bag_meta" target="_blank" class="custom-link download-link">
|
||||
<span class="link-icon">↓</span>
|
||||
<span class="link-text">下载 Ego_motion CSV文件</span>
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="Raw_gps数据">
|
||||
<a :href="data.raw_gps" target="_blank" class="custom-link download-link">
|
||||
<span class="link-icon">↓</span>
|
||||
<span class="link-text">下载 Raw_gps CSV文件</span>
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="Raw_imu数据">
|
||||
<a :href="data.raw_imu" target="_blank" class="custom-link download-link">
|
||||
<span class="link-icon">↓</span>
|
||||
<span class="link-text">下载 Raw_imu CSV文件</span>
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="Vehicle_wheel数据">
|
||||
<a :href="data.vehicle_wheel" target="_blank" class="custom-link download-link">
|
||||
<span class="link-icon">↓</span>
|
||||
<span class="link-text">下载 Vehicle_wheel CSV文件</span>
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getBagDetailApi } from '#/api/core/baglist';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// const data1 = {
|
||||
// bag_name: 'PL162802_event_all_time_event_20250416-193015_0.bag',
|
||||
// datetime: 'Wed, 16 Apr 2025 11:30:15 GMT',
|
||||
// update_time: 'Fri, 20 Jun 2025 02:33:28 GMT',
|
||||
// is_active_data: true,
|
||||
// is_decoded: true,
|
||||
// ld_annotated: 1,
|
||||
// od_annotated: 1,
|
||||
// reserved_json: {
|
||||
// bag_meta: 'https://cla-dev.ca4ad.com/cdi/data/61af489c7c497d2af13228c78b5882e4',
|
||||
// bag_player: 'https://mviz-dev.ca4ad.com/player/v4/?bag_md5=61af489c7c497d2af13228c78b5882e4',
|
||||
// },
|
||||
// raw_gps: 'https://b-perception-e2e-1318950322.cos.ap-shanghai-adc.myqcloud.com/',
|
||||
// raw_imu: 'https://b-perception-e2e-1318950322.cos.ap-shanghai-adc.myqcloud.com/',
|
||||
// vehicle_wheel: 'https://b-perception-e2e-1318950322.cos.ap-shanghai-adc.myqcloud.com',
|
||||
// };
|
||||
|
||||
// 标注状态文本转换函数
|
||||
const getAnnotationText = (status) => {
|
||||
const statusMap = {
|
||||
0: '未标注',
|
||||
1: '自动标注',
|
||||
2: '手工标注',
|
||||
3: '自动+手工标注'
|
||||
};
|
||||
return statusMap[status] || '未知状态';
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
// const detailData = ref();
|
||||
// const fetchDeatil = async () => {
|
||||
// const fileName = props.bagId;
|
||||
// try {
|
||||
// const res = await getBagDetailApi({ bagname: [fileName] });
|
||||
// if (res && res.fileName) {
|
||||
// detailData.value = res.fileName;
|
||||
// } else {
|
||||
// ElMessage({
|
||||
// message: '获取数据失败:响应中无文件信息',
|
||||
// type: 'error',
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// ElMessage({
|
||||
// message: `请求出错:${error || '未知错误'}`,
|
||||
// type: 'error',
|
||||
// });
|
||||
// console.error('获取 bag 详情失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box-card {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0 24px 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-scroll-container {
|
||||
padding-right: 1px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
::v-deep(.custom-descriptions .el-descriptions__table) {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
::v-deep(.custom-descriptions .el-descriptions__cell) {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
::v-deep(.custom-descriptions .el-descriptions__label) {
|
||||
width: 160px;
|
||||
min-width: 120px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
/* 链接样式优化 */
|
||||
.custom-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.custom-link:hover {
|
||||
background-color: #ecf5ff;
|
||||
color: #1e88e5;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
/* 符号图标样式 */
|
||||
.link-icon,
|
||||
.tag-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.external-icon {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 不同类型链接的特色样式 */
|
||||
.video-link {
|
||||
color: #21b573;
|
||||
}
|
||||
|
||||
.video-link:hover {
|
||||
background-color: #f0f9ea;
|
||||
color: #13ce66;
|
||||
}
|
||||
|
||||
.meta-link {
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.meta-link:hover {
|
||||
background-color: #fff8e1;
|
||||
color: #e68a00;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 标注标签样式 */
|
||||
.annotation-tag {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.info-scroll-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.info-scroll-container::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
365
apps/web-ele/src/views/labelmanage/modifylabel/index.vue
Normal file
365
apps/web-ele/src/views/labelmanage/modifylabel/index.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Row } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { onMounted, reactive, ref, type UnwrapRef } from 'vue';
|
||||
|
||||
import {
|
||||
getFstTreeTagApi,
|
||||
getCreateLevelOneApi,
|
||||
getUpdateAnnotationApi,
|
||||
getAddSubLabelApi,
|
||||
getSyncFstApi
|
||||
} from '#/api/core/label';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const labelList = ref([])
|
||||
const isFormDisabled = ref(true)
|
||||
|
||||
interface RowType {
|
||||
date: string;
|
||||
id: number;
|
||||
name: string;
|
||||
parentId: null | number;
|
||||
name_cn: string;
|
||||
annotation?: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', width: 70 },
|
||||
{ field: 'name', title: 'STS name', treeNode: true },
|
||||
{ field: 'level', title: 'Level' },
|
||||
{ field: 'date', title: 'Update time' },
|
||||
{ field: 'annotation', title: 'Annotation' },
|
||||
{ slots: { default: 'action' }, title: '操作', width: 250 },
|
||||
],
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
const isDialogVisible = ref(false)
|
||||
const selectedRow = ref<any>()
|
||||
const isDialogAddVisible = ref(false)
|
||||
const isDialogAddOneVisible = ref(false)
|
||||
const isDialogSyncVisible = ref(false)
|
||||
const rootTreeValue = ref()
|
||||
|
||||
const rootTree = [
|
||||
{
|
||||
value: 'FST_Driving',
|
||||
label: 'FST_Driving',
|
||||
},
|
||||
{
|
||||
value: 'APA FST',
|
||||
label: 'APA FST',
|
||||
},]
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await getFstTreeTagApi();
|
||||
if (response.data) {
|
||||
gridApi.setGridOptions({ data: response.data });
|
||||
labelList.value = response.data
|
||||
} else {
|
||||
console.error('API 返回的数据格式不正确,期望是一个数组:', response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求 FST 树形标签数据出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const expandAll = () => {
|
||||
gridApi.grid?.setAllTreeExpand(true);
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
gridApi.grid?.setAllTreeExpand(false);
|
||||
};
|
||||
|
||||
// 切换指定行的展开/收起状态
|
||||
const toggleExpandRow = (row: RowType) => {
|
||||
if (gridApi.grid) {
|
||||
gridApi.grid?.toggleTreeExpand(row);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
interface FormState {
|
||||
name: string;
|
||||
level?: number;
|
||||
parentName: string;
|
||||
annotation?: string;
|
||||
}
|
||||
const formStateEdit: UnwrapRef<FormState> = reactive({
|
||||
name: '',
|
||||
level: 0,
|
||||
parentName: '',
|
||||
annotation: ''
|
||||
});
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
selectedRow.value = row
|
||||
const { name, level, annotation } = row
|
||||
formStateEdit.name = name
|
||||
formStateEdit.level = level
|
||||
formStateEdit.annotation = annotation
|
||||
formStateEdit.parentName = labelList.value.find(item => item.id == row.parentId)?.name ? labelList.value.find(item => item.id == row.parentId)?.name : '无父级'
|
||||
isDialogVisible.value = true
|
||||
// console.log(formStateEdit)
|
||||
}
|
||||
|
||||
const handleAdd = (row: any) => {
|
||||
const { name, level } = row
|
||||
formStateEdit.name = ''
|
||||
formStateEdit.level = level + 1
|
||||
formStateEdit.annotation = ''
|
||||
formStateEdit.parentName = name
|
||||
isDialogAddVisible.value = true
|
||||
}
|
||||
|
||||
// 同步数据到rootdb数据库
|
||||
const handlesyncFst = async (row: any) => {
|
||||
// console.log(row)
|
||||
const { name, level } = row
|
||||
if (level > 1) {
|
||||
formStateEdit.parentName = labelList.value.find(item => item.id == row.parentId)?.name
|
||||
}
|
||||
formStateEdit.name = name
|
||||
formStateEdit.level = level
|
||||
isDialogSyncVisible.value = true
|
||||
// console.log(23, formStateEdit)
|
||||
}
|
||||
|
||||
const addLevelOneLabel = () => {
|
||||
formStateEdit.name = ''
|
||||
formStateEdit.level = 1
|
||||
formStateEdit.annotation = ''
|
||||
isDialogAddOneVisible.value = true
|
||||
}
|
||||
|
||||
const updateLabel = async () => {
|
||||
try {
|
||||
await getUpdateAnnotationApi(formStateEdit)
|
||||
isDialogAddVisible.value = false
|
||||
await fetchData();
|
||||
ElMessage({
|
||||
message: '修改成功!',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
isDialogAddVisible.value = false
|
||||
ElMessage({
|
||||
message: `修改失败!${error}`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const addSubLabel = async () => {
|
||||
// console.log(365, formStateEdit)
|
||||
try {
|
||||
await getAddSubLabelApi(formStateEdit)
|
||||
isDialogAddVisible.value = false
|
||||
await fetchData();
|
||||
ElMessage({
|
||||
message: '修改成功!',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
isDialogAddVisible.value = false
|
||||
ElMessage({
|
||||
message: `修改失败!${error}`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const addLabelOne = async () => {
|
||||
try {
|
||||
await getCreateLevelOneApi(formStateEdit)
|
||||
isDialogAddOneVisible.value = false
|
||||
await fetchData();
|
||||
ElMessage({
|
||||
message: '新增成功!',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
isDialogAddOneVisible.value = false
|
||||
ElMessage({
|
||||
message: `新增失败!${error}`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const syncLabel = async () => {
|
||||
const params = { name: "", parent_name: '' }
|
||||
const { name, level, parentName } = formStateEdit
|
||||
if (level > 1) {
|
||||
params.parent_name = parentName
|
||||
} else {
|
||||
params.parent_name = rootTreeValue.value
|
||||
}
|
||||
params.name = name;
|
||||
|
||||
try {
|
||||
await getSyncFstApi(params)
|
||||
isDialogSyncVisible.value = false
|
||||
await fetchData();
|
||||
ElMessage({
|
||||
message: '新增成功!',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
isDialogSyncVisible.value = false
|
||||
ElMessage({
|
||||
message: `新增失败!${error}`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Grid table-title="Fst标签" class="m-2">
|
||||
<template #toolbar-tools>
|
||||
<Button class="mr-2" danger @click="addLevelOneLabel">
|
||||
新建一级标签
|
||||
</Button>
|
||||
<Button class="mr-2" type="primary" @click="expandAll">
|
||||
展开全部
|
||||
</Button>
|
||||
<Button type="primary" @click="collapseAll"> 折叠全部 </Button>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button class="mr-1" type="link" @click="handleEdit(row)">编辑</Button>
|
||||
<Button class="mr-1" type="link" @click="handleAdd(row)">新增下级</Button>
|
||||
<Button class="mr-1" danger type="link" @click="handlesyncFst(row)">同步</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
<el-dialog v-model="isDialogVisible" title="修改标签" width="500px" center>
|
||||
<el-form :model="formStateEdit" style="max-width: 400px; margin: 0 auto;">
|
||||
<el-form-item label="名称" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.name" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="级别" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.level" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父标签名称" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.parentName" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.annotation" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="isDialogVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="updateLabel">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="isDialogAddVisible" title="新建子标签" width="500px" center>
|
||||
<el-form :model="formStateEdit" style="max-width: 400px; margin: 0 auto;">
|
||||
<el-form-item label="新标签名称" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父标签名称" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.parentName" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="级别" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.level" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.annotation" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="isDialogAddVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="addSubLabel">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="isDialogAddOneVisible" title="新建一级标签" width="500px" center>
|
||||
<el-form :model="formStateEdit" style="max-width: 400px; margin: 0 auto;">
|
||||
<el-form-item label="新标签名称" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.name" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="父标签名称" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.parentName" :disabled="isFormDisabled" />
|
||||
</el-form-item> -->
|
||||
<el-form-item label="级别" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.level" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.annotation" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="isDialogAddOneVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="addLabelOne">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="isDialogSyncVisible" title="同步标签" width="500px" center>
|
||||
<el-form :model="formStateEdit" style="max-width: 400px; margin: 0 auto;">
|
||||
<el-form-item label="当前标签" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.name" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="父标签" :wrapper-col="{ span: 15 }" v-show="formStateEdit.level == 1">
|
||||
<el-select v-model="rootTreeValue" placeholder="请选择根标签" style="width: 400px">
|
||||
<el-option v-for="item in rootTree" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="父标签" :wrapper-col="{ span: 15 }" v-show="formStateEdit.level > 1">
|
||||
<el-input v-model="formStateEdit.parentName" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前级别" :wrapper-col="{ span: 15 }">
|
||||
<el-input v-model="formStateEdit.level" :disabled="isFormDisabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="isDialogSyncVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="syncLabel">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</Page>
|
||||
</template>
|
||||
130
apps/web-ele/src/views/labelmanage/showremotelabel/index.vue
Normal file
130
apps/web-ele/src/views/labelmanage/showremotelabel/index.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<Page auto-content-height description="可查看rootdb中的fst数据" title="RootDB中的Fst数据结构">
|
||||
<div class="json-viewer tree m-2">
|
||||
<div class="mt-1 ml-8 button-example">
|
||||
<div class="button-row">
|
||||
<!-- 去掉loading图标,保留disabled防止重复点击 -->
|
||||
<el-button type="primary" @click="showDriving" :disabled="loading">
|
||||
Driving
|
||||
</el-button>
|
||||
<el-button type="success" @click="showParking" :disabled="loading">
|
||||
Parking
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 文字提示:替代loading图标的反馈 -->
|
||||
<div v-if="loading" class="loading-text">
|
||||
数据加载中,请稍候...
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 ml-8" v-if="displayTree">
|
||||
<el-tree :data="treeData" node-key="id" :default-expand-all="true" :props="treeProps" />
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 ml-8" v-else>
|
||||
<p>请点击上方按钮查看对应类型的Fst数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElTree, ElButton } from 'element-plus';
|
||||
import { getFstByTypeApi } from '#/api/core/rootdata';
|
||||
|
||||
// 状态变量(去掉了currentType,保留核心逻辑)
|
||||
const displayTree = ref(false);
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
// 树控件配置
|
||||
const treeProps = ref({
|
||||
label: "label",
|
||||
children: "children"
|
||||
});
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
|
||||
/**
|
||||
* 根据类型获取Fst数据
|
||||
*/
|
||||
const fetchFstByType = async (type: any) => {
|
||||
try {
|
||||
loading.value = true; // 仅用于禁用按钮和显示文字提示
|
||||
errorMessage.value = '';
|
||||
|
||||
const response = await getFstByTypeApi(type);
|
||||
treeData.value = [response.data];
|
||||
displayTree.value = true;
|
||||
} catch (err) {
|
||||
errorMessage.value = `获取${type === 'driving' ? '驾驶' : '停车'}数据失败,请稍后重试`;
|
||||
console.error('获取Fst数据失败:', err);
|
||||
} finally {
|
||||
loading.value = false; // 无论成功失败,都关闭加载状态
|
||||
}
|
||||
};
|
||||
|
||||
// 显示驾驶数据
|
||||
const showDriving = async () => {
|
||||
await fetchFstByType({ type: 'driving' });
|
||||
};
|
||||
|
||||
// 显示停车数据
|
||||
const showParking = async () => {
|
||||
await fetchFstByType({ type: 'parking' });
|
||||
};
|
||||
|
||||
// 初始化 - 在页面挂载后自动加载driving数据
|
||||
onMounted(() => {
|
||||
// 页面加载时自动调用showDriving方法展示驾驶数据
|
||||
showDriving();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-viewer.tree {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.button-example {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
/* 缩小按钮与提示文字的间距 */
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 固定按钮宽度,彻底避免尺寸变化 */
|
||||
.button-row .el-button {
|
||||
min-width: 100px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 加载状态文字提示 */
|
||||
.loading-text {
|
||||
color: #606266;
|
||||
/* 浅灰色,不刺眼 */
|
||||
font-size: 14px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #f56c6c;
|
||||
margin-top: 10px;
|
||||
padding: 8px;
|
||||
background-color: #fef0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
351
apps/web-ele/src/views/usercenter/usermanage.vue
Normal file
351
apps/web-ele/src/views/usercenter/usermanage.vue
Normal file
@@ -0,0 +1,351 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
createUserApi,
|
||||
getRolesApi,
|
||||
getUsersApi,
|
||||
updateUserApi,
|
||||
} from '#/api/core/user';
|
||||
|
||||
type RoleOption = {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type UserRow = {
|
||||
id: number;
|
||||
username: string;
|
||||
status: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
roles: RoleOption[];
|
||||
};
|
||||
|
||||
const users = ref<UserRow[]>([]);
|
||||
const roles = ref<RoleOption[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const creating = ref(false);
|
||||
|
||||
const roleDialogVisible = ref(false);
|
||||
const selectedRoleIds = ref<number[]>([]);
|
||||
const editingUser = ref<UserRow | null>(null);
|
||||
|
||||
const createDialogVisible = ref(false);
|
||||
const createForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
role_ids: [] as number[],
|
||||
});
|
||||
|
||||
const normalizeUsers = (rows: any[]) => {
|
||||
const userMap = new Map<number, UserRow>();
|
||||
const roleMap = new Map<number, RoleOption>();
|
||||
|
||||
rows.forEach((row) => {
|
||||
const userId = Number(row.id);
|
||||
if (!userMap.has(userId)) {
|
||||
userMap.set(userId, {
|
||||
id: userId,
|
||||
username: row.username ?? '',
|
||||
status: Number(row.status ?? 0),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
roles: [],
|
||||
});
|
||||
}
|
||||
|
||||
const roleId = Number(row.role_id ?? row.roleId);
|
||||
const roleName = row.name ?? row.role_name ?? row.role;
|
||||
if (!Number.isFinite(roleId) || !roleName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const role: RoleOption = {
|
||||
id: roleId,
|
||||
name: String(roleName),
|
||||
description: row.description,
|
||||
};
|
||||
roleMap.set(roleId, role);
|
||||
|
||||
const user = userMap.get(userId)!;
|
||||
if (!user.roles.some((item) => item.id === roleId)) {
|
||||
user.roles.push(role);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
users: Array.from(userMap.values()),
|
||||
roles: Array.from(roleMap.values()),
|
||||
};
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getUsersApi();
|
||||
const rows = Array.isArray(res) ? res : res?.data ?? [];
|
||||
const normalized = normalizeUsers(rows);
|
||||
users.value = normalized.users;
|
||||
if (!roles.value.length && normalized.roles.length) {
|
||||
roles.value = normalized.roles;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to load users: ${error}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
const res = await getRolesApi();
|
||||
const list = Array.isArray(res) ? res : res?.data ?? [];
|
||||
roles.value = list.map((item: any) => ({
|
||||
id: Number(item.id),
|
||||
name: String(item.name ?? ''),
|
||||
description: item.description,
|
||||
}));
|
||||
} catch (error) {
|
||||
// Fallback to roles derived from user list.
|
||||
}
|
||||
};
|
||||
|
||||
const openRoleDialog = (row: UserRow) => {
|
||||
editingUser.value = row;
|
||||
selectedRoleIds.value = row.roles.map((role) => role.id);
|
||||
roleDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const saveRoles = async () => {
|
||||
if (!editingUser.value) {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateUserApi({
|
||||
user_id: editingUser.value.id,
|
||||
role_ids: selectedRoleIds.value,
|
||||
});
|
||||
ElMessage.success('Roles updated');
|
||||
roleDialogVisible.value = false;
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to update roles: ${error}`);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
createForm.username = '';
|
||||
createForm.password = '';
|
||||
createForm.role_ids = [];
|
||||
createDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const submitCreateUser = async () => {
|
||||
if (!createForm.username.trim() || !createForm.password.trim()) {
|
||||
ElMessage.warning('请输入用户名和密码');
|
||||
return;
|
||||
}
|
||||
creating.value = true;
|
||||
try {
|
||||
await createUserApi({
|
||||
username: createForm.username.trim(),
|
||||
password: createForm.password,
|
||||
role_ids: createForm.role_ids,
|
||||
});
|
||||
ElMessage.success('用户创建成功');
|
||||
createDialogVisible.value = false;
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
ElMessage.error(`创建失败:${error}`);
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleUserStatus = async (row: UserRow) => {
|
||||
const nextStatus = row.status === 1 ? 0 : 1;
|
||||
const actionLabel = nextStatus === 1 ? '启用' : '禁用';
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定${actionLabel}用户 ${row.username} 吗?`,
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUserApi({
|
||||
user_id: row.id,
|
||||
status: nextStatus,
|
||||
});
|
||||
ElMessage.success(`用户已${actionLabel}`);
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
ElMessage.error(`${actionLabel}失败:${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadRoles(), loadUsers()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="用户管理">
|
||||
<ElCard>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="text-sm text-gray-600">
|
||||
Total: {{ users.length }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ElButton type="primary" @click="openCreateDialog">
|
||||
创建用户
|
||||
</ElButton>
|
||||
<ElButton :loading="loading" @click="loadUsers">
|
||||
刷新
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElTable :data="users" stripe v-loading="loading">
|
||||
<ElTableColumn prop="id" label="ID" width="80" />
|
||||
<ElTableColumn prop="username" label="Username" min-width="160" />
|
||||
<ElTableColumn label="Roles" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.roles.length">
|
||||
<ElTag
|
||||
v-for="role in row.roles"
|
||||
:key="role.id"
|
||||
class="mr-1"
|
||||
type="info"
|
||||
>
|
||||
{{ role.name }}
|
||||
</ElTag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Status" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? 'Active' : 'Disabled' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Actions" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<ElButton size="small" @click="openRoleDialog(row)">
|
||||
设置角色
|
||||
</ElButton>
|
||||
<ElButton
|
||||
size="small"
|
||||
:type="row.status === 1 ? 'danger' : 'success'"
|
||||
@click="toggleUserStatus(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '禁用' : '启用' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDialog v-model="createDialogVisible" title="创建用户" width="420">
|
||||
<ElForm label-width="100px">
|
||||
<ElFormItem label="用户名">
|
||||
<ElInput v-model="createForm.username" placeholder="请输入用户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码">
|
||||
<ElInput
|
||||
v-model="createForm.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="至少8位,包含数字"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色">
|
||||
<ElSelect
|
||||
v-model="createForm.role_ids"
|
||||
class="w-full"
|
||||
multiple
|
||||
placeholder="选择角色(可选)"
|
||||
>
|
||||
<ElOption
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton :loading="creating" type="primary" @click="submitCreateUser">
|
||||
创建
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog v-model="roleDialogVisible" title="Set Roles" width="420">
|
||||
<div v-if="editingUser" class="mb-3 text-sm text-gray-600">
|
||||
User: {{ editingUser.username }}
|
||||
</div>
|
||||
<ElForm label-width="100px">
|
||||
<ElFormItem label="Roles">
|
||||
<ElSelect
|
||||
v-model="selectedRoleIds"
|
||||
class="w-full"
|
||||
multiple
|
||||
placeholder="Select roles"
|
||||
>
|
||||
<ElOption
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="roleDialogVisible = false">Cancel</ElButton>
|
||||
<ElButton :loading="saving" type="primary" @click="saveRoles">
|
||||
Save
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</Page>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user