添加数据库文件
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -47,9 +47,6 @@ coverage.xml
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
data/db.sqlite3
|
||||
data/*.sqlite3
|
||||
media/
|
||||
staticfiles/
|
||||
static_root/
|
||||
|
||||
BIN
data/db.sqlite3
Normal file
BIN
data/db.sqlite3
Normal file
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
# EditorConfig配置文件
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{js,ts,vue}]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.{css,scss,sass}]
|
||||
indent_size = 2
|
||||
24
hertz_server_diango_ui/.gitignore
vendored
24
hertz_server_diango_ui/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"hash": "300f9ef5",
|
||||
"configHash": "f25ca102",
|
||||
"lockfileHash": "b99b8a02",
|
||||
"browserHash": "68dc98de",
|
||||
"optimized": {
|
||||
"@ant-design/icons-vue": {
|
||||
"src": "../../node_modules/@ant-design/icons-vue/es/index.js",
|
||||
"file": "@ant-design_icons-vue.js",
|
||||
"fileHash": "9f30893d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"ant-design-vue": {
|
||||
"src": "../../node_modules/ant-design-vue/es/index.js",
|
||||
"file": "ant-design-vue.js",
|
||||
"fileHash": "19e51532",
|
||||
"needsInterop": false
|
||||
},
|
||||
"ant-design-vue/es/locale/en_US": {
|
||||
"src": "../../node_modules/ant-design-vue/es/locale/en_US.js",
|
||||
"file": "ant-design-vue_es_locale_en_US.js",
|
||||
"fileHash": "2bb8ba2e",
|
||||
"needsInterop": false
|
||||
},
|
||||
"ant-design-vue/es/locale/zh_CN": {
|
||||
"src": "../../node_modules/ant-design-vue/es/locale/zh_CN.js",
|
||||
"file": "ant-design-vue_es_locale_zh_CN.js",
|
||||
"fileHash": "a20449b1",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../node_modules/axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "4d6a6f42",
|
||||
"needsInterop": false
|
||||
},
|
||||
"dayjs": {
|
||||
"src": "../../node_modules/dayjs/dayjs.min.js",
|
||||
"file": "dayjs.js",
|
||||
"fileHash": "ab9a8365",
|
||||
"needsInterop": true
|
||||
},
|
||||
"jszip": {
|
||||
"src": "../../node_modules/jszip/dist/jszip.min.js",
|
||||
"file": "jszip.js",
|
||||
"fileHash": "8a18241e",
|
||||
"needsInterop": true
|
||||
},
|
||||
"pinia": {
|
||||
"src": "../../node_modules/pinia/dist/pinia.mjs",
|
||||
"file": "pinia.js",
|
||||
"fileHash": "bbe3ba96",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "a721dff8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue-i18n": {
|
||||
"src": "../../node_modules/vue-i18n/dist/vue-i18n.mjs",
|
||||
"file": "vue-i18n.js",
|
||||
"fileHash": "a5b2fc34",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue-router": {
|
||||
"src": "../../node_modules/vue-router/dist/vue-router.mjs",
|
||||
"file": "vue-router.js",
|
||||
"fileHash": "62efce1c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"ant-design-vue/es": {
|
||||
"src": "../../node_modules/ant-design-vue/es/index.js",
|
||||
"file": "ant-design-vue_es.js",
|
||||
"fileHash": "726b9d57",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-AYVSL3LM": {
|
||||
"file": "chunk-AYVSL3LM.js"
|
||||
},
|
||||
"chunk-UD4KTM7R": {
|
||||
"file": "chunk-UD4KTM7R.js"
|
||||
},
|
||||
"chunk-ZC474HKL": {
|
||||
"file": "chunk-ZC474HKL.js"
|
||||
},
|
||||
"chunk-F5SAIAJ6": {
|
||||
"file": "chunk-F5SAIAJ6.js"
|
||||
},
|
||||
"chunk-YODRZMZT": {
|
||||
"file": "chunk-YODRZMZT.js"
|
||||
},
|
||||
"chunk-IJSSJBZ4": {
|
||||
"file": "chunk-IJSSJBZ4.js"
|
||||
},
|
||||
"chunk-XCUFKJYR": {
|
||||
"file": "chunk-XCUFKJYR.js"
|
||||
},
|
||||
"chunk-Y7TKRIWE": {
|
||||
"file": "chunk-Y7TKRIWE.js"
|
||||
},
|
||||
"chunk-PR4QN5HX": {
|
||||
"file": "chunk-PR4QN5HX.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import {
|
||||
AnchorLink_default,
|
||||
AutoCompleteOptGroup,
|
||||
AutoCompleteOption,
|
||||
Avatar_default,
|
||||
BreadcrumbItem_default,
|
||||
BreadcrumbSeparator_default,
|
||||
Button_default,
|
||||
CheckableTag_default,
|
||||
CollapsePanel_default,
|
||||
ColumnGroup_default,
|
||||
Column_default,
|
||||
DescriptionsItem,
|
||||
DirectoryTree_default,
|
||||
Divider_default,
|
||||
FormItemContext_default,
|
||||
FormItem_default,
|
||||
Grid_default,
|
||||
Group_default,
|
||||
Group_default2,
|
||||
Group_default3,
|
||||
Group_default4,
|
||||
Image_default,
|
||||
Input_default,
|
||||
ItemGroup_default,
|
||||
ItemMeta_default,
|
||||
Item_default,
|
||||
LayoutContent,
|
||||
LayoutFooter,
|
||||
LayoutHeader,
|
||||
LayoutSider,
|
||||
Link_default,
|
||||
MentionsOption,
|
||||
MenuItem_default,
|
||||
Meta_default,
|
||||
MonthPicker,
|
||||
Paragraph_default,
|
||||
Password_default,
|
||||
PreviewGroup_default,
|
||||
QuarterPicker,
|
||||
RadioButton_default,
|
||||
RangePicker,
|
||||
Ribbon_default,
|
||||
Search_default,
|
||||
SelectOptGroup,
|
||||
SelectOption,
|
||||
StatisticCountdown,
|
||||
Step,
|
||||
SubMenu_default,
|
||||
TabPane_default,
|
||||
TableSummary,
|
||||
TableSummaryCell,
|
||||
TableSummaryRow,
|
||||
TextArea_default,
|
||||
Text_default,
|
||||
TimeRangePicker,
|
||||
TimelineItem_default,
|
||||
Title_default,
|
||||
Title_default2,
|
||||
TreeNode,
|
||||
TreeSelectNode,
|
||||
UploadDragger,
|
||||
WeekPicker,
|
||||
affix_default,
|
||||
alert_default,
|
||||
anchor_default,
|
||||
auto_complete_default,
|
||||
avatar_default,
|
||||
back_top_default,
|
||||
badge_default,
|
||||
breadcrumb_default,
|
||||
button_default,
|
||||
button_group_default,
|
||||
calendar_default,
|
||||
card_default,
|
||||
carousel_default,
|
||||
cascader_default,
|
||||
checkbox_default,
|
||||
col_default,
|
||||
collapse_default,
|
||||
comment_default,
|
||||
config_provider_default,
|
||||
date_picker_default,
|
||||
descriptions_default,
|
||||
divider_default,
|
||||
drawer_default,
|
||||
dropdown_button_default,
|
||||
dropdown_default,
|
||||
empty_default,
|
||||
es_default,
|
||||
form_default,
|
||||
grid_default,
|
||||
image_default,
|
||||
input_default,
|
||||
input_number_default,
|
||||
install,
|
||||
layout_default,
|
||||
list_default,
|
||||
locale_provider_default,
|
||||
mentions_default,
|
||||
menu_default,
|
||||
message_default,
|
||||
modal_default,
|
||||
notification_default,
|
||||
page_header_default,
|
||||
pagination_default,
|
||||
popconfirm_default,
|
||||
popover_default,
|
||||
progress_default,
|
||||
radio_default,
|
||||
rate_default,
|
||||
result_default,
|
||||
row_default,
|
||||
select_default,
|
||||
skeleton_default,
|
||||
slider_default,
|
||||
space_default,
|
||||
spin_default,
|
||||
statistic_default,
|
||||
steps_default,
|
||||
switch_default,
|
||||
table_default,
|
||||
tabs_default,
|
||||
tag_default,
|
||||
time_picker_default,
|
||||
timeline_default,
|
||||
tooltip_default,
|
||||
transfer_default,
|
||||
tree_default,
|
||||
tree_select_default,
|
||||
typography_default,
|
||||
upload_default,
|
||||
version_default
|
||||
} from "./chunk-UD4KTM7R.js";
|
||||
import "./chunk-ZC474HKL.js";
|
||||
import "./chunk-F5SAIAJ6.js";
|
||||
import "./chunk-YODRZMZT.js";
|
||||
import "./chunk-IJSSJBZ4.js";
|
||||
import "./chunk-XCUFKJYR.js";
|
||||
import "./chunk-Y7TKRIWE.js";
|
||||
import "./chunk-PR4QN5HX.js";
|
||||
export {
|
||||
affix_default as Affix,
|
||||
alert_default as Alert,
|
||||
anchor_default as Anchor,
|
||||
AnchorLink_default as AnchorLink,
|
||||
auto_complete_default as AutoComplete,
|
||||
AutoCompleteOptGroup,
|
||||
AutoCompleteOption,
|
||||
avatar_default as Avatar,
|
||||
Group_default as AvatarGroup,
|
||||
back_top_default as BackTop,
|
||||
badge_default as Badge,
|
||||
Ribbon_default as BadgeRibbon,
|
||||
breadcrumb_default as Breadcrumb,
|
||||
BreadcrumbItem_default as BreadcrumbItem,
|
||||
BreadcrumbSeparator_default as BreadcrumbSeparator,
|
||||
button_default as Button,
|
||||
button_group_default as ButtonGroup,
|
||||
calendar_default as Calendar,
|
||||
card_default as Card,
|
||||
Grid_default as CardGrid,
|
||||
Meta_default as CardMeta,
|
||||
carousel_default as Carousel,
|
||||
cascader_default as Cascader,
|
||||
CheckableTag_default as CheckableTag,
|
||||
checkbox_default as Checkbox,
|
||||
Group_default3 as CheckboxGroup,
|
||||
col_default as Col,
|
||||
collapse_default as Collapse,
|
||||
CollapsePanel_default as CollapsePanel,
|
||||
comment_default as Comment,
|
||||
config_provider_default as ConfigProvider,
|
||||
date_picker_default as DatePicker,
|
||||
descriptions_default as Descriptions,
|
||||
DescriptionsItem,
|
||||
DirectoryTree_default as DirectoryTree,
|
||||
divider_default as Divider,
|
||||
drawer_default as Drawer,
|
||||
dropdown_default as Dropdown,
|
||||
dropdown_button_default as DropdownButton,
|
||||
empty_default as Empty,
|
||||
form_default as Form,
|
||||
FormItem_default as FormItem,
|
||||
FormItemContext_default as FormItemRest,
|
||||
grid_default as Grid,
|
||||
image_default as Image,
|
||||
PreviewGroup_default as ImagePreviewGroup,
|
||||
input_default as Input,
|
||||
Group_default4 as InputGroup,
|
||||
input_number_default as InputNumber,
|
||||
Password_default as InputPassword,
|
||||
Search_default as InputSearch,
|
||||
layout_default as Layout,
|
||||
LayoutContent,
|
||||
LayoutFooter,
|
||||
LayoutHeader,
|
||||
LayoutSider,
|
||||
list_default as List,
|
||||
Item_default as ListItem,
|
||||
ItemMeta_default as ListItemMeta,
|
||||
locale_provider_default as LocaleProvider,
|
||||
mentions_default as Mentions,
|
||||
MentionsOption,
|
||||
menu_default as Menu,
|
||||
Divider_default as MenuDivider,
|
||||
MenuItem_default as MenuItem,
|
||||
ItemGroup_default as MenuItemGroup,
|
||||
modal_default as Modal,
|
||||
MonthPicker,
|
||||
page_header_default as PageHeader,
|
||||
pagination_default as Pagination,
|
||||
popconfirm_default as Popconfirm,
|
||||
popover_default as Popover,
|
||||
progress_default as Progress,
|
||||
QuarterPicker,
|
||||
radio_default as Radio,
|
||||
RadioButton_default as RadioButton,
|
||||
Group_default2 as RadioGroup,
|
||||
RangePicker,
|
||||
rate_default as Rate,
|
||||
result_default as Result,
|
||||
row_default as Row,
|
||||
select_default as Select,
|
||||
SelectOptGroup,
|
||||
SelectOption,
|
||||
skeleton_default as Skeleton,
|
||||
Avatar_default as SkeletonAvatar,
|
||||
Button_default as SkeletonButton,
|
||||
Image_default as SkeletonImage,
|
||||
Input_default as SkeletonInput,
|
||||
Title_default as SkeletonTitle,
|
||||
slider_default as Slider,
|
||||
space_default as Space,
|
||||
spin_default as Spin,
|
||||
statistic_default as Statistic,
|
||||
StatisticCountdown,
|
||||
Step,
|
||||
steps_default as Steps,
|
||||
SubMenu_default as SubMenu,
|
||||
switch_default as Switch,
|
||||
TabPane_default as TabPane,
|
||||
table_default as Table,
|
||||
Column_default as TableColumn,
|
||||
ColumnGroup_default as TableColumnGroup,
|
||||
TableSummary,
|
||||
TableSummaryCell,
|
||||
TableSummaryRow,
|
||||
tabs_default as Tabs,
|
||||
tag_default as Tag,
|
||||
TextArea_default as Textarea,
|
||||
time_picker_default as TimePicker,
|
||||
TimeRangePicker,
|
||||
timeline_default as Timeline,
|
||||
TimelineItem_default as TimelineItem,
|
||||
tooltip_default as Tooltip,
|
||||
transfer_default as Transfer,
|
||||
tree_default as Tree,
|
||||
TreeNode,
|
||||
tree_select_default as TreeSelect,
|
||||
TreeSelectNode,
|
||||
typography_default as Typography,
|
||||
Link_default as TypographyLink,
|
||||
Paragraph_default as TypographyParagraph,
|
||||
Text_default as TypographyText,
|
||||
Title_default2 as TypographyTitle,
|
||||
upload_default as Upload,
|
||||
UploadDragger,
|
||||
WeekPicker,
|
||||
es_default as default,
|
||||
install,
|
||||
message_default as message,
|
||||
notification_default as notification,
|
||||
version_default as version
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import {
|
||||
AnchorLink_default,
|
||||
AutoCompleteOptGroup,
|
||||
AutoCompleteOption,
|
||||
Avatar_default,
|
||||
BreadcrumbItem_default,
|
||||
BreadcrumbSeparator_default,
|
||||
Button_default,
|
||||
CheckableTag_default,
|
||||
CollapsePanel_default,
|
||||
ColumnGroup_default,
|
||||
Column_default,
|
||||
DescriptionsItem,
|
||||
DirectoryTree_default,
|
||||
Divider_default,
|
||||
FormItemContext_default,
|
||||
FormItem_default,
|
||||
Grid_default,
|
||||
Group_default,
|
||||
Group_default2,
|
||||
Group_default3,
|
||||
Group_default4,
|
||||
Image_default,
|
||||
Input_default,
|
||||
ItemGroup_default,
|
||||
ItemMeta_default,
|
||||
Item_default,
|
||||
LayoutContent,
|
||||
LayoutFooter,
|
||||
LayoutHeader,
|
||||
LayoutSider,
|
||||
Link_default,
|
||||
MentionsOption,
|
||||
MenuItem_default,
|
||||
Meta_default,
|
||||
MonthPicker,
|
||||
Paragraph_default,
|
||||
Password_default,
|
||||
PreviewGroup_default,
|
||||
QuarterPicker,
|
||||
RadioButton_default,
|
||||
RangePicker,
|
||||
Ribbon_default,
|
||||
Search_default,
|
||||
SelectOptGroup,
|
||||
SelectOption,
|
||||
StatisticCountdown,
|
||||
Step,
|
||||
SubMenu_default,
|
||||
TabPane_default,
|
||||
TableSummary,
|
||||
TableSummaryCell,
|
||||
TableSummaryRow,
|
||||
TextArea_default,
|
||||
Text_default,
|
||||
TimeRangePicker,
|
||||
TimelineItem_default,
|
||||
Title_default,
|
||||
Title_default2,
|
||||
TreeNode,
|
||||
TreeSelectNode,
|
||||
UploadDragger,
|
||||
WeekPicker,
|
||||
affix_default,
|
||||
alert_default,
|
||||
anchor_default,
|
||||
auto_complete_default,
|
||||
avatar_default,
|
||||
back_top_default,
|
||||
badge_default,
|
||||
breadcrumb_default,
|
||||
button_default,
|
||||
button_group_default,
|
||||
calendar_default,
|
||||
card_default,
|
||||
carousel_default,
|
||||
cascader_default,
|
||||
checkbox_default,
|
||||
col_default,
|
||||
collapse_default,
|
||||
comment_default,
|
||||
config_provider_default,
|
||||
date_picker_default,
|
||||
descriptions_default,
|
||||
divider_default,
|
||||
drawer_default,
|
||||
dropdown_button_default,
|
||||
dropdown_default,
|
||||
empty_default,
|
||||
es_default,
|
||||
form_default,
|
||||
grid_default,
|
||||
image_default,
|
||||
input_default,
|
||||
input_number_default,
|
||||
install,
|
||||
layout_default,
|
||||
list_default,
|
||||
locale_provider_default,
|
||||
mentions_default,
|
||||
menu_default,
|
||||
message_default,
|
||||
modal_default,
|
||||
notification_default,
|
||||
page_header_default,
|
||||
pagination_default,
|
||||
popconfirm_default,
|
||||
popover_default,
|
||||
progress_default,
|
||||
radio_default,
|
||||
rate_default,
|
||||
result_default,
|
||||
row_default,
|
||||
select_default,
|
||||
skeleton_default,
|
||||
slider_default,
|
||||
space_default,
|
||||
spin_default,
|
||||
statistic_default,
|
||||
steps_default,
|
||||
switch_default,
|
||||
table_default,
|
||||
tabs_default,
|
||||
tag_default,
|
||||
time_picker_default,
|
||||
timeline_default,
|
||||
tooltip_default,
|
||||
transfer_default,
|
||||
tree_default,
|
||||
tree_select_default,
|
||||
typography_default,
|
||||
upload_default,
|
||||
version_default
|
||||
} from "./chunk-UD4KTM7R.js";
|
||||
import "./chunk-ZC474HKL.js";
|
||||
import "./chunk-F5SAIAJ6.js";
|
||||
import "./chunk-YODRZMZT.js";
|
||||
import "./chunk-IJSSJBZ4.js";
|
||||
import "./chunk-XCUFKJYR.js";
|
||||
import "./chunk-Y7TKRIWE.js";
|
||||
import "./chunk-PR4QN5HX.js";
|
||||
export {
|
||||
affix_default as Affix,
|
||||
alert_default as Alert,
|
||||
anchor_default as Anchor,
|
||||
AnchorLink_default as AnchorLink,
|
||||
auto_complete_default as AutoComplete,
|
||||
AutoCompleteOptGroup,
|
||||
AutoCompleteOption,
|
||||
avatar_default as Avatar,
|
||||
Group_default as AvatarGroup,
|
||||
back_top_default as BackTop,
|
||||
badge_default as Badge,
|
||||
Ribbon_default as BadgeRibbon,
|
||||
breadcrumb_default as Breadcrumb,
|
||||
BreadcrumbItem_default as BreadcrumbItem,
|
||||
BreadcrumbSeparator_default as BreadcrumbSeparator,
|
||||
button_default as Button,
|
||||
button_group_default as ButtonGroup,
|
||||
calendar_default as Calendar,
|
||||
card_default as Card,
|
||||
Grid_default as CardGrid,
|
||||
Meta_default as CardMeta,
|
||||
carousel_default as Carousel,
|
||||
cascader_default as Cascader,
|
||||
CheckableTag_default as CheckableTag,
|
||||
checkbox_default as Checkbox,
|
||||
Group_default3 as CheckboxGroup,
|
||||
col_default as Col,
|
||||
collapse_default as Collapse,
|
||||
CollapsePanel_default as CollapsePanel,
|
||||
comment_default as Comment,
|
||||
config_provider_default as ConfigProvider,
|
||||
date_picker_default as DatePicker,
|
||||
descriptions_default as Descriptions,
|
||||
DescriptionsItem,
|
||||
DirectoryTree_default as DirectoryTree,
|
||||
divider_default as Divider,
|
||||
drawer_default as Drawer,
|
||||
dropdown_default as Dropdown,
|
||||
dropdown_button_default as DropdownButton,
|
||||
empty_default as Empty,
|
||||
form_default as Form,
|
||||
FormItem_default as FormItem,
|
||||
FormItemContext_default as FormItemRest,
|
||||
grid_default as Grid,
|
||||
image_default as Image,
|
||||
PreviewGroup_default as ImagePreviewGroup,
|
||||
input_default as Input,
|
||||
Group_default4 as InputGroup,
|
||||
input_number_default as InputNumber,
|
||||
Password_default as InputPassword,
|
||||
Search_default as InputSearch,
|
||||
layout_default as Layout,
|
||||
LayoutContent,
|
||||
LayoutFooter,
|
||||
LayoutHeader,
|
||||
LayoutSider,
|
||||
list_default as List,
|
||||
Item_default as ListItem,
|
||||
ItemMeta_default as ListItemMeta,
|
||||
locale_provider_default as LocaleProvider,
|
||||
mentions_default as Mentions,
|
||||
MentionsOption,
|
||||
menu_default as Menu,
|
||||
Divider_default as MenuDivider,
|
||||
MenuItem_default as MenuItem,
|
||||
ItemGroup_default as MenuItemGroup,
|
||||
modal_default as Modal,
|
||||
MonthPicker,
|
||||
page_header_default as PageHeader,
|
||||
pagination_default as Pagination,
|
||||
popconfirm_default as Popconfirm,
|
||||
popover_default as Popover,
|
||||
progress_default as Progress,
|
||||
QuarterPicker,
|
||||
radio_default as Radio,
|
||||
RadioButton_default as RadioButton,
|
||||
Group_default2 as RadioGroup,
|
||||
RangePicker,
|
||||
rate_default as Rate,
|
||||
result_default as Result,
|
||||
row_default as Row,
|
||||
select_default as Select,
|
||||
SelectOptGroup,
|
||||
SelectOption,
|
||||
skeleton_default as Skeleton,
|
||||
Avatar_default as SkeletonAvatar,
|
||||
Button_default as SkeletonButton,
|
||||
Image_default as SkeletonImage,
|
||||
Input_default as SkeletonInput,
|
||||
Title_default as SkeletonTitle,
|
||||
slider_default as Slider,
|
||||
space_default as Space,
|
||||
spin_default as Spin,
|
||||
statistic_default as Statistic,
|
||||
StatisticCountdown,
|
||||
Step,
|
||||
steps_default as Steps,
|
||||
SubMenu_default as SubMenu,
|
||||
switch_default as Switch,
|
||||
TabPane_default as TabPane,
|
||||
table_default as Table,
|
||||
Column_default as TableColumn,
|
||||
ColumnGroup_default as TableColumnGroup,
|
||||
TableSummary,
|
||||
TableSummaryCell,
|
||||
TableSummaryRow,
|
||||
tabs_default as Tabs,
|
||||
tag_default as Tag,
|
||||
TextArea_default as Textarea,
|
||||
time_picker_default as TimePicker,
|
||||
TimeRangePicker,
|
||||
timeline_default as Timeline,
|
||||
TimelineItem_default as TimelineItem,
|
||||
tooltip_default as Tooltip,
|
||||
transfer_default as Transfer,
|
||||
tree_default as Tree,
|
||||
TreeNode,
|
||||
tree_select_default as TreeSelect,
|
||||
TreeSelectNode,
|
||||
typography_default as Typography,
|
||||
Link_default as TypographyLink,
|
||||
Paragraph_default as TypographyParagraph,
|
||||
Text_default as TypographyText,
|
||||
Title_default2 as TypographyTitle,
|
||||
upload_default as Upload,
|
||||
UploadDragger,
|
||||
WeekPicker,
|
||||
es_default as default,
|
||||
install,
|
||||
message_default as message,
|
||||
notification_default as notification,
|
||||
version_default as version
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import {
|
||||
en_US_default4 as en_US_default
|
||||
} from "./chunk-F5SAIAJ6.js";
|
||||
import "./chunk-IJSSJBZ4.js";
|
||||
import "./chunk-PR4QN5HX.js";
|
||||
export {
|
||||
en_US_default as default
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
import {
|
||||
zh_CN_default
|
||||
} from "./chunk-YODRZMZT.js";
|
||||
import {
|
||||
_objectSpread2
|
||||
} from "./chunk-IJSSJBZ4.js";
|
||||
import "./chunk-PR4QN5HX.js";
|
||||
|
||||
// node_modules/ant-design-vue/es/vc-picker/locale/zh_CN.js
|
||||
var locale = {
|
||||
locale: "zh_CN",
|
||||
today: "今天",
|
||||
now: "此刻",
|
||||
backToToday: "返回今天",
|
||||
ok: "确定",
|
||||
timeSelect: "选择时间",
|
||||
dateSelect: "选择日期",
|
||||
weekSelect: "选择周",
|
||||
clear: "清除",
|
||||
month: "月",
|
||||
year: "年",
|
||||
previousMonth: "上个月 (翻页上键)",
|
||||
nextMonth: "下个月 (翻页下键)",
|
||||
monthSelect: "选择月份",
|
||||
yearSelect: "选择年份",
|
||||
decadeSelect: "选择年代",
|
||||
yearFormat: "YYYY年",
|
||||
dayFormat: "D日",
|
||||
dateFormat: "YYYY年M月D日",
|
||||
dateTimeFormat: "YYYY年M月D日 HH时mm分ss秒",
|
||||
previousYear: "上一年 (Control键加左方向键)",
|
||||
nextYear: "下一年 (Control键加右方向键)",
|
||||
previousDecade: "上一年代",
|
||||
nextDecade: "下一年代",
|
||||
previousCentury: "上一世纪",
|
||||
nextCentury: "下一世纪"
|
||||
};
|
||||
var zh_CN_default2 = locale;
|
||||
|
||||
// node_modules/ant-design-vue/es/time-picker/locale/zh_CN.js
|
||||
var locale2 = {
|
||||
placeholder: "请选择时间",
|
||||
rangePlaceholder: ["开始时间", "结束时间"]
|
||||
};
|
||||
var zh_CN_default3 = locale2;
|
||||
|
||||
// node_modules/ant-design-vue/es/date-picker/locale/zh_CN.js
|
||||
var locale3 = {
|
||||
lang: _objectSpread2({
|
||||
placeholder: "请选择日期",
|
||||
yearPlaceholder: "请选择年份",
|
||||
quarterPlaceholder: "请选择季度",
|
||||
monthPlaceholder: "请选择月份",
|
||||
weekPlaceholder: "请选择周",
|
||||
rangePlaceholder: ["开始日期", "结束日期"],
|
||||
rangeYearPlaceholder: ["开始年份", "结束年份"],
|
||||
rangeMonthPlaceholder: ["开始月份", "结束月份"],
|
||||
rangeQuarterPlaceholder: ["开始季度", "结束季度"],
|
||||
rangeWeekPlaceholder: ["开始周", "结束周"]
|
||||
}, zh_CN_default2),
|
||||
timePickerLocale: _objectSpread2({}, zh_CN_default3)
|
||||
};
|
||||
locale3.lang.ok = "确定";
|
||||
var zh_CN_default4 = locale3;
|
||||
|
||||
// node_modules/ant-design-vue/es/calendar/locale/zh_CN.js
|
||||
var zh_CN_default5 = zh_CN_default4;
|
||||
|
||||
// node_modules/ant-design-vue/es/locale/zh_CN.js
|
||||
var typeTemplate = "${label}不是一个有效的${type}";
|
||||
var localeValues = {
|
||||
locale: "zh-cn",
|
||||
Pagination: zh_CN_default,
|
||||
DatePicker: zh_CN_default4,
|
||||
TimePicker: zh_CN_default3,
|
||||
Calendar: zh_CN_default5,
|
||||
// locales for all components
|
||||
global: {
|
||||
placeholder: "请选择"
|
||||
},
|
||||
Table: {
|
||||
filterTitle: "筛选",
|
||||
filterConfirm: "确定",
|
||||
filterReset: "重置",
|
||||
filterEmptyText: "无筛选项",
|
||||
filterCheckall: "全选",
|
||||
filterSearchPlaceholder: "在筛选项中搜索",
|
||||
selectAll: "全选当页",
|
||||
selectInvert: "反选当页",
|
||||
selectNone: "清空所有",
|
||||
selectionAll: "全选所有",
|
||||
sortTitle: "排序",
|
||||
expand: "展开行",
|
||||
collapse: "关闭行",
|
||||
triggerDesc: "点击降序",
|
||||
triggerAsc: "点击升序",
|
||||
cancelSort: "取消排序"
|
||||
},
|
||||
Modal: {
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
justOkText: "知道了"
|
||||
},
|
||||
Popconfirm: {
|
||||
cancelText: "取消",
|
||||
okText: "确定"
|
||||
},
|
||||
Transfer: {
|
||||
searchPlaceholder: "请输入搜索内容",
|
||||
itemUnit: "项",
|
||||
itemsUnit: "项",
|
||||
remove: "删除",
|
||||
selectCurrent: "全选当页",
|
||||
removeCurrent: "删除当页",
|
||||
selectAll: "全选所有",
|
||||
removeAll: "删除全部",
|
||||
selectInvert: "反选当页"
|
||||
},
|
||||
Upload: {
|
||||
uploading: "文件上传中",
|
||||
removeFile: "删除文件",
|
||||
uploadError: "上传错误",
|
||||
previewFile: "预览文件",
|
||||
downloadFile: "下载文件"
|
||||
},
|
||||
Empty: {
|
||||
description: "暂无数据"
|
||||
},
|
||||
Icon: {
|
||||
icon: "图标"
|
||||
},
|
||||
Text: {
|
||||
edit: "编辑",
|
||||
copy: "复制",
|
||||
copied: "复制成功",
|
||||
expand: "展开"
|
||||
},
|
||||
PageHeader: {
|
||||
back: "返回"
|
||||
},
|
||||
Form: {
|
||||
optional: "(可选)",
|
||||
defaultValidateMessages: {
|
||||
default: "字段验证错误${label}",
|
||||
required: "请输入${label}",
|
||||
enum: "${label}必须是其中一个[${enum}]",
|
||||
whitespace: "${label}不能为空字符",
|
||||
date: {
|
||||
format: "${label}日期格式无效",
|
||||
parse: "${label}不能转换为日期",
|
||||
invalid: "${label}是一个无效日期"
|
||||
},
|
||||
types: {
|
||||
string: typeTemplate,
|
||||
method: typeTemplate,
|
||||
array: typeTemplate,
|
||||
object: typeTemplate,
|
||||
number: typeTemplate,
|
||||
date: typeTemplate,
|
||||
boolean: typeTemplate,
|
||||
integer: typeTemplate,
|
||||
float: typeTemplate,
|
||||
regexp: typeTemplate,
|
||||
email: typeTemplate,
|
||||
url: typeTemplate,
|
||||
hex: typeTemplate
|
||||
},
|
||||
string: {
|
||||
len: "${label}须为${len}个字符",
|
||||
min: "${label}最少${min}个字符",
|
||||
max: "${label}最多${max}个字符",
|
||||
range: "${label}须在${min}-${max}字符之间"
|
||||
},
|
||||
number: {
|
||||
len: "${label}必须等于${len}",
|
||||
min: "${label}最小值为${min}",
|
||||
max: "${label}最大值为${max}",
|
||||
range: "${label}须在${min}-${max}之间"
|
||||
},
|
||||
array: {
|
||||
len: "须为${len}个${label}",
|
||||
min: "最少${min}个${label}",
|
||||
max: "最多${max}个${label}",
|
||||
range: "${label}数量须在${min}-${max}之间"
|
||||
},
|
||||
pattern: {
|
||||
mismatch: "${label}与模式不匹配${pattern}"
|
||||
}
|
||||
}
|
||||
},
|
||||
Image: {
|
||||
preview: "预览"
|
||||
}
|
||||
};
|
||||
var zh_CN_default6 = localeValues;
|
||||
export {
|
||||
zh_CN_default6 as default
|
||||
};
|
||||
//# sourceMappingURL=ant-design-vue_es_locale_zh_CN.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,162 +0,0 @@
|
||||
// node_modules/@vue/devtools-api/lib/esm/env.js
|
||||
function getDevtoolsGlobalHook() {
|
||||
return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
||||
}
|
||||
function getTarget() {
|
||||
return typeof navigator !== "undefined" && typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : {};
|
||||
}
|
||||
var isProxyAvailable = typeof Proxy === "function";
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/const.js
|
||||
var HOOK_SETUP = "devtools-plugin:setup";
|
||||
var HOOK_PLUGIN_SETTINGS_SET = "plugin:settings:set";
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/time.js
|
||||
var supported;
|
||||
var perf;
|
||||
function isPerformanceSupported() {
|
||||
var _a;
|
||||
if (supported !== void 0) {
|
||||
return supported;
|
||||
}
|
||||
if (typeof window !== "undefined" && window.performance) {
|
||||
supported = true;
|
||||
perf = window.performance;
|
||||
} else if (typeof globalThis !== "undefined" && ((_a = globalThis.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {
|
||||
supported = true;
|
||||
perf = globalThis.perf_hooks.performance;
|
||||
} else {
|
||||
supported = false;
|
||||
}
|
||||
return supported;
|
||||
}
|
||||
function now() {
|
||||
return isPerformanceSupported() ? perf.now() : Date.now();
|
||||
}
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/proxy.js
|
||||
var ApiProxy = class {
|
||||
constructor(plugin, hook) {
|
||||
this.target = null;
|
||||
this.targetQueue = [];
|
||||
this.onQueue = [];
|
||||
this.plugin = plugin;
|
||||
this.hook = hook;
|
||||
const defaultSettings = {};
|
||||
if (plugin.settings) {
|
||||
for (const id in plugin.settings) {
|
||||
const item = plugin.settings[id];
|
||||
defaultSettings[id] = item.defaultValue;
|
||||
}
|
||||
}
|
||||
const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;
|
||||
let currentSettings = Object.assign({}, defaultSettings);
|
||||
try {
|
||||
const raw = localStorage.getItem(localSettingsSaveId);
|
||||
const data = JSON.parse(raw);
|
||||
Object.assign(currentSettings, data);
|
||||
} catch (e) {
|
||||
}
|
||||
this.fallbacks = {
|
||||
getSettings() {
|
||||
return currentSettings;
|
||||
},
|
||||
setSettings(value) {
|
||||
try {
|
||||
localStorage.setItem(localSettingsSaveId, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
}
|
||||
currentSettings = value;
|
||||
},
|
||||
now() {
|
||||
return now();
|
||||
}
|
||||
};
|
||||
if (hook) {
|
||||
hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {
|
||||
if (pluginId === this.plugin.id) {
|
||||
this.fallbacks.setSettings(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.proxiedOn = new Proxy({}, {
|
||||
get: (_target, prop) => {
|
||||
if (this.target) {
|
||||
return this.target.on[prop];
|
||||
} else {
|
||||
return (...args) => {
|
||||
this.onQueue.push({
|
||||
method: prop,
|
||||
args
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
this.proxiedTarget = new Proxy({}, {
|
||||
get: (_target, prop) => {
|
||||
if (this.target) {
|
||||
return this.target[prop];
|
||||
} else if (prop === "on") {
|
||||
return this.proxiedOn;
|
||||
} else if (Object.keys(this.fallbacks).includes(prop)) {
|
||||
return (...args) => {
|
||||
this.targetQueue.push({
|
||||
method: prop,
|
||||
args,
|
||||
resolve: () => {
|
||||
}
|
||||
});
|
||||
return this.fallbacks[prop](...args);
|
||||
};
|
||||
} else {
|
||||
return (...args) => {
|
||||
return new Promise((resolve) => {
|
||||
this.targetQueue.push({
|
||||
method: prop,
|
||||
args,
|
||||
resolve
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
async setRealTarget(target) {
|
||||
this.target = target;
|
||||
for (const item of this.onQueue) {
|
||||
this.target.on[item.method](...item.args);
|
||||
}
|
||||
for (const item of this.targetQueue) {
|
||||
item.resolve(await this.target[item.method](...item.args));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/index.js
|
||||
function setupDevtoolsPlugin(pluginDescriptor, setupFn) {
|
||||
const descriptor = pluginDescriptor;
|
||||
const target = getTarget();
|
||||
const hook = getDevtoolsGlobalHook();
|
||||
const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;
|
||||
if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {
|
||||
hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);
|
||||
} else {
|
||||
const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;
|
||||
const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];
|
||||
list.push({
|
||||
pluginDescriptor: descriptor,
|
||||
setupFn,
|
||||
proxy
|
||||
});
|
||||
if (proxy) {
|
||||
setupFn(proxy.proxiedTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
setupDevtoolsPlugin
|
||||
};
|
||||
//# sourceMappingURL=chunk-AYVSL3LM.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,220 +0,0 @@
|
||||
import {
|
||||
_objectSpread2
|
||||
} from "./chunk-IJSSJBZ4.js";
|
||||
|
||||
// node_modules/ant-design-vue/es/vc-pagination/locale/en_US.js
|
||||
var en_US_default = {
|
||||
// Options.jsx
|
||||
items_per_page: "/ page",
|
||||
jump_to: "Go to",
|
||||
jump_to_confirm: "confirm",
|
||||
page: "",
|
||||
// Pagination.jsx
|
||||
prev_page: "Previous Page",
|
||||
next_page: "Next Page",
|
||||
prev_5: "Previous 5 Pages",
|
||||
next_5: "Next 5 Pages",
|
||||
prev_3: "Previous 3 Pages",
|
||||
next_3: "Next 3 Pages"
|
||||
};
|
||||
|
||||
// node_modules/ant-design-vue/es/vc-picker/locale/en_US.js
|
||||
var locale = {
|
||||
locale: "en_US",
|
||||
today: "Today",
|
||||
now: "Now",
|
||||
backToToday: "Back to today",
|
||||
ok: "Ok",
|
||||
clear: "Clear",
|
||||
month: "Month",
|
||||
year: "Year",
|
||||
timeSelect: "select time",
|
||||
dateSelect: "select date",
|
||||
weekSelect: "Choose a week",
|
||||
monthSelect: "Choose a month",
|
||||
yearSelect: "Choose a year",
|
||||
decadeSelect: "Choose a decade",
|
||||
yearFormat: "YYYY",
|
||||
dateFormat: "M/D/YYYY",
|
||||
dayFormat: "D",
|
||||
dateTimeFormat: "M/D/YYYY HH:mm:ss",
|
||||
monthBeforeYear: true,
|
||||
previousMonth: "Previous month (PageUp)",
|
||||
nextMonth: "Next month (PageDown)",
|
||||
previousYear: "Last year (Control + left)",
|
||||
nextYear: "Next year (Control + right)",
|
||||
previousDecade: "Last decade",
|
||||
nextDecade: "Next decade",
|
||||
previousCentury: "Last century",
|
||||
nextCentury: "Next century"
|
||||
};
|
||||
var en_US_default2 = locale;
|
||||
|
||||
// node_modules/ant-design-vue/es/time-picker/locale/en_US.js
|
||||
var locale2 = {
|
||||
placeholder: "Select time",
|
||||
rangePlaceholder: ["Start time", "End time"]
|
||||
};
|
||||
var en_US_default3 = locale2;
|
||||
|
||||
// node_modules/ant-design-vue/es/date-picker/locale/en_US.js
|
||||
var locale3 = {
|
||||
lang: _objectSpread2({
|
||||
placeholder: "Select date",
|
||||
yearPlaceholder: "Select year",
|
||||
quarterPlaceholder: "Select quarter",
|
||||
monthPlaceholder: "Select month",
|
||||
weekPlaceholder: "Select week",
|
||||
rangePlaceholder: ["Start date", "End date"],
|
||||
rangeYearPlaceholder: ["Start year", "End year"],
|
||||
rangeQuarterPlaceholder: ["Start quarter", "End quarter"],
|
||||
rangeMonthPlaceholder: ["Start month", "End month"],
|
||||
rangeWeekPlaceholder: ["Start week", "End week"]
|
||||
}, en_US_default2),
|
||||
timePickerLocale: _objectSpread2({}, en_US_default3)
|
||||
};
|
||||
var en_US_default4 = locale3;
|
||||
|
||||
// node_modules/ant-design-vue/es/calendar/locale/en_US.js
|
||||
var en_US_default5 = en_US_default4;
|
||||
|
||||
// node_modules/ant-design-vue/es/locale/default.js
|
||||
var typeTemplate = "${label} is not a valid ${type}";
|
||||
var localeValues = {
|
||||
locale: "en",
|
||||
Pagination: en_US_default,
|
||||
DatePicker: en_US_default4,
|
||||
TimePicker: en_US_default3,
|
||||
Calendar: en_US_default5,
|
||||
global: {
|
||||
placeholder: "Please select"
|
||||
},
|
||||
Table: {
|
||||
filterTitle: "Filter menu",
|
||||
filterConfirm: "OK",
|
||||
filterReset: "Reset",
|
||||
filterEmptyText: "No filters",
|
||||
filterCheckall: "Select all items",
|
||||
filterSearchPlaceholder: "Search in filters",
|
||||
emptyText: "No data",
|
||||
selectAll: "Select current page",
|
||||
selectInvert: "Invert current page",
|
||||
selectNone: "Clear all data",
|
||||
selectionAll: "Select all data",
|
||||
sortTitle: "Sort",
|
||||
expand: "Expand row",
|
||||
collapse: "Collapse row",
|
||||
triggerDesc: "Click to sort descending",
|
||||
triggerAsc: "Click to sort ascending",
|
||||
cancelSort: "Click to cancel sorting"
|
||||
},
|
||||
Modal: {
|
||||
okText: "OK",
|
||||
cancelText: "Cancel",
|
||||
justOkText: "OK"
|
||||
},
|
||||
Popconfirm: {
|
||||
okText: "OK",
|
||||
cancelText: "Cancel"
|
||||
},
|
||||
Transfer: {
|
||||
titles: ["", ""],
|
||||
searchPlaceholder: "Search here",
|
||||
itemUnit: "item",
|
||||
itemsUnit: "items",
|
||||
remove: "Remove",
|
||||
selectCurrent: "Select current page",
|
||||
removeCurrent: "Remove current page",
|
||||
selectAll: "Select all data",
|
||||
removeAll: "Remove all data",
|
||||
selectInvert: "Invert current page"
|
||||
},
|
||||
Upload: {
|
||||
uploading: "Uploading...",
|
||||
removeFile: "Remove file",
|
||||
uploadError: "Upload error",
|
||||
previewFile: "Preview file",
|
||||
downloadFile: "Download file"
|
||||
},
|
||||
Empty: {
|
||||
description: "No Data"
|
||||
},
|
||||
Icon: {
|
||||
icon: "icon"
|
||||
},
|
||||
Text: {
|
||||
edit: "Edit",
|
||||
copy: "Copy",
|
||||
copied: "Copied",
|
||||
expand: "Expand"
|
||||
},
|
||||
PageHeader: {
|
||||
back: "Back"
|
||||
},
|
||||
Form: {
|
||||
optional: "(optional)",
|
||||
defaultValidateMessages: {
|
||||
default: "Field validation error for ${label}",
|
||||
required: "Please enter ${label}",
|
||||
enum: "${label} must be one of [${enum}]",
|
||||
whitespace: "${label} cannot be a blank character",
|
||||
date: {
|
||||
format: "${label} date format is invalid",
|
||||
parse: "${label} cannot be converted to a date",
|
||||
invalid: "${label} is an invalid date"
|
||||
},
|
||||
types: {
|
||||
string: typeTemplate,
|
||||
method: typeTemplate,
|
||||
array: typeTemplate,
|
||||
object: typeTemplate,
|
||||
number: typeTemplate,
|
||||
date: typeTemplate,
|
||||
boolean: typeTemplate,
|
||||
integer: typeTemplate,
|
||||
float: typeTemplate,
|
||||
regexp: typeTemplate,
|
||||
email: typeTemplate,
|
||||
url: typeTemplate,
|
||||
hex: typeTemplate
|
||||
},
|
||||
string: {
|
||||
len: "${label} must be ${len} characters",
|
||||
min: "${label} must be at least ${min} characters",
|
||||
max: "${label} must be up to ${max} characters",
|
||||
range: "${label} must be between ${min}-${max} characters"
|
||||
},
|
||||
number: {
|
||||
len: "${label} must be equal to ${len}",
|
||||
min: "${label} must be minimum ${min}",
|
||||
max: "${label} must be maximum ${max}",
|
||||
range: "${label} must be between ${min}-${max}"
|
||||
},
|
||||
array: {
|
||||
len: "Must be ${len} ${label}",
|
||||
min: "At least ${min} ${label}",
|
||||
max: "At most ${max} ${label}",
|
||||
range: "The amount of ${label} must be between ${min}-${max}"
|
||||
},
|
||||
pattern: {
|
||||
mismatch: "${label} does not match the pattern ${pattern}"
|
||||
}
|
||||
}
|
||||
},
|
||||
Image: {
|
||||
preview: "Preview"
|
||||
}
|
||||
};
|
||||
var default_default = localeValues;
|
||||
|
||||
// node_modules/ant-design-vue/es/locale/en_US.js
|
||||
var en_US_default6 = default_default;
|
||||
|
||||
export {
|
||||
en_US_default,
|
||||
en_US_default4 as en_US_default2,
|
||||
en_US_default5 as en_US_default3,
|
||||
default_default,
|
||||
en_US_default6 as en_US_default4
|
||||
};
|
||||
//# sourceMappingURL=chunk-F5SAIAJ6.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,68 +0,0 @@
|
||||
// node_modules/@babel/runtime/helpers/esm/typeof.js
|
||||
function _typeof(o) {
|
||||
"@babel/helpers - typeof";
|
||||
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o2) {
|
||||
return typeof o2;
|
||||
} : function(o2) {
|
||||
return o2 && "function" == typeof Symbol && o2.constructor === Symbol && o2 !== Symbol.prototype ? "symbol" : typeof o2;
|
||||
}, _typeof(o);
|
||||
}
|
||||
|
||||
// node_modules/@babel/runtime/helpers/esm/toPrimitive.js
|
||||
function toPrimitive(t, r) {
|
||||
if ("object" != _typeof(t) || !t) return t;
|
||||
var e = t[Symbol.toPrimitive];
|
||||
if (void 0 !== e) {
|
||||
var i = e.call(t, r || "default");
|
||||
if ("object" != _typeof(i)) return i;
|
||||
throw new TypeError("@@toPrimitive must return a primitive value.");
|
||||
}
|
||||
return ("string" === r ? String : Number)(t);
|
||||
}
|
||||
|
||||
// node_modules/@babel/runtime/helpers/esm/toPropertyKey.js
|
||||
function toPropertyKey(t) {
|
||||
var i = toPrimitive(t, "string");
|
||||
return "symbol" == _typeof(i) ? i : i + "";
|
||||
}
|
||||
|
||||
// node_modules/@babel/runtime/helpers/esm/defineProperty.js
|
||||
function _defineProperty(e, r, t) {
|
||||
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
||||
value: t,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
}) : e[r] = t, e;
|
||||
}
|
||||
|
||||
// node_modules/@babel/runtime/helpers/esm/objectSpread2.js
|
||||
function ownKeys(e, r) {
|
||||
var t = Object.keys(e);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var o = Object.getOwnPropertySymbols(e);
|
||||
r && (o = o.filter(function(r2) {
|
||||
return Object.getOwnPropertyDescriptor(e, r2).enumerable;
|
||||
})), t.push.apply(t, o);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
function _objectSpread2(e) {
|
||||
for (var r = 1; r < arguments.length; r++) {
|
||||
var t = null != arguments[r] ? arguments[r] : {};
|
||||
r % 2 ? ownKeys(Object(t), true).forEach(function(r2) {
|
||||
_defineProperty(e, r2, t[r2]);
|
||||
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r2) {
|
||||
Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
|
||||
});
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
export {
|
||||
_typeof,
|
||||
toPropertyKey,
|
||||
_defineProperty,
|
||||
_objectSpread2
|
||||
};
|
||||
//# sourceMappingURL=chunk-IJSSJBZ4.js.map
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../node_modules/@babel/runtime/helpers/esm/typeof.js", "../../node_modules/@babel/runtime/helpers/esm/toPrimitive.js", "../../node_modules/@babel/runtime/helpers/esm/toPropertyKey.js", "../../node_modules/@babel/runtime/helpers/esm/defineProperty.js", "../../node_modules/@babel/runtime/helpers/esm/objectSpread2.js"],
|
||||
"sourcesContent": ["function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, _typeof(o);\n}\nexport { _typeof as default };", "import _typeof from \"./typeof.js\";\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nexport { toPrimitive as default };", "import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nexport { toPropertyKey as default };", "import toPropertyKey from \"./toPropertyKey.js\";\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nexport { _defineProperty as default };", "import defineProperty from \"./defineProperty.js\";\nfunction ownKeys(e, r) {\n var t = Object.keys(e);\n if (Object.getOwnPropertySymbols) {\n var o = Object.getOwnPropertySymbols(e);\n r && (o = o.filter(function (r) {\n return Object.getOwnPropertyDescriptor(e, r).enumerable;\n })), t.push.apply(t, o);\n }\n return t;\n}\nfunction _objectSpread2(e) {\n for (var r = 1; r < arguments.length; r++) {\n var t = null != arguments[r] ? arguments[r] : {};\n r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {\n defineProperty(e, r, t[r]);\n }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {\n Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));\n });\n }\n return e;\n}\nexport { _objectSpread2 as default };"],
|
||||
"mappings": ";AAAA,SAAS,QAAQ,GAAG;AAClB;AAEA,SAAO,UAAU,cAAc,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,SAAUA,IAAG;AAChG,WAAO,OAAOA;AAAA,EAChB,IAAI,SAAUA,IAAG;AACf,WAAOA,MAAK,cAAc,OAAO,UAAUA,GAAE,gBAAgB,UAAUA,OAAM,OAAO,YAAY,WAAW,OAAOA;AAAA,EACpH,GAAG,QAAQ,CAAC;AACd;;;ACPA,SAAS,YAAY,GAAG,GAAG;AACzB,MAAI,YAAY,QAAQ,CAAC,KAAK,CAAC,EAAG,QAAO;AACzC,MAAI,IAAI,EAAE,OAAO,WAAW;AAC5B,MAAI,WAAW,GAAG;AAChB,QAAI,IAAI,EAAE,KAAK,GAAG,KAAK,SAAS;AAChC,QAAI,YAAY,QAAQ,CAAC,EAAG,QAAO;AACnC,UAAM,IAAI,UAAU,8CAA8C;AAAA,EACpE;AACA,UAAQ,aAAa,IAAI,SAAS,QAAQ,CAAC;AAC7C;;;ACRA,SAAS,cAAc,GAAG;AACxB,MAAI,IAAI,YAAY,GAAG,QAAQ;AAC/B,SAAO,YAAY,QAAQ,CAAC,IAAI,IAAI,IAAI;AAC1C;;;ACJA,SAAS,gBAAgB,GAAG,GAAG,GAAG;AAChC,UAAQ,IAAI,cAAc,CAAC,MAAM,IAAI,OAAO,eAAe,GAAG,GAAG;AAAA,IAC/D,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,UAAU;AAAA,EACZ,CAAC,IAAI,EAAE,CAAC,IAAI,GAAG;AACjB;;;ACPA,SAAS,QAAQ,GAAG,GAAG;AACrB,MAAI,IAAI,OAAO,KAAK,CAAC;AACrB,MAAI,OAAO,uBAAuB;AAChC,QAAI,IAAI,OAAO,sBAAsB,CAAC;AACtC,UAAM,IAAI,EAAE,OAAO,SAAUC,IAAG;AAC9B,aAAO,OAAO,yBAAyB,GAAGA,EAAC,EAAE;AAAA,IAC/C,CAAC,IAAI,EAAE,KAAK,MAAM,GAAG,CAAC;AAAA,EACxB;AACA,SAAO;AACT;AACA,SAAS,eAAe,GAAG;AACzB,WAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AACzC,QAAI,IAAI,QAAQ,UAAU,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC;AAC/C,QAAI,IAAI,QAAQ,OAAO,CAAC,GAAG,IAAE,EAAE,QAAQ,SAAUA,IAAG;AAClD,sBAAe,GAAGA,IAAG,EAAEA,EAAC,CAAC;AAAA,IAC3B,CAAC,IAAI,OAAO,4BAA4B,OAAO,iBAAiB,GAAG,OAAO,0BAA0B,CAAC,CAAC,IAAI,QAAQ,OAAO,CAAC,CAAC,EAAE,QAAQ,SAAUA,IAAG;AAChJ,aAAO,eAAe,GAAGA,IAAG,OAAO,yBAAyB,GAAGA,EAAC,CAAC;AAAA,IACnE,CAAC;AAAA,EACH;AACA,SAAO;AACT;",
|
||||
"names": ["o", "r"]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
||||
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
||||
}) : x)(function(x) {
|
||||
if (typeof require !== "undefined") return require.apply(this, arguments);
|
||||
throw Error('Dynamic require of "' + x + '" is not supported');
|
||||
});
|
||||
var __commonJS = (cb, mod) => function __require2() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
|
||||
export {
|
||||
__require,
|
||||
__commonJS,
|
||||
__export,
|
||||
__toESM
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,288 +0,0 @@
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-PR4QN5HX.js";
|
||||
|
||||
// node_modules/dayjs/dayjs.min.js
|
||||
var require_dayjs_min = __commonJS({
|
||||
"node_modules/dayjs/dayjs.min.js"(exports, module) {
|
||||
!(function(t, e) {
|
||||
"object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e();
|
||||
})(exports, (function() {
|
||||
"use strict";
|
||||
var t = 1e3, e = 6e4, n = 36e5, r = "millisecond", i = "second", s = "minute", u = "hour", a = "day", o = "week", c = "month", f = "quarter", h = "year", d = "date", l = "Invalid Date", $ = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, y = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, M = { name: "en", weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), ordinal: function(t2) {
|
||||
var e2 = ["th", "st", "nd", "rd"], n2 = t2 % 100;
|
||||
return "[" + t2 + (e2[(n2 - 20) % 10] || e2[n2] || e2[0]) + "]";
|
||||
} }, m = function(t2, e2, n2) {
|
||||
var r2 = String(t2);
|
||||
return !r2 || r2.length >= e2 ? t2 : "" + Array(e2 + 1 - r2.length).join(n2) + t2;
|
||||
}, v = { s: m, z: function(t2) {
|
||||
var e2 = -t2.utcOffset(), n2 = Math.abs(e2), r2 = Math.floor(n2 / 60), i2 = n2 % 60;
|
||||
return (e2 <= 0 ? "+" : "-") + m(r2, 2, "0") + ":" + m(i2, 2, "0");
|
||||
}, m: function t2(e2, n2) {
|
||||
if (e2.date() < n2.date()) return -t2(n2, e2);
|
||||
var r2 = 12 * (n2.year() - e2.year()) + (n2.month() - e2.month()), i2 = e2.clone().add(r2, c), s2 = n2 - i2 < 0, u2 = e2.clone().add(r2 + (s2 ? -1 : 1), c);
|
||||
return +(-(r2 + (n2 - i2) / (s2 ? i2 - u2 : u2 - i2)) || 0);
|
||||
}, a: function(t2) {
|
||||
return t2 < 0 ? Math.ceil(t2) || 0 : Math.floor(t2);
|
||||
}, p: function(t2) {
|
||||
return { M: c, y: h, w: o, d: a, D: d, h: u, m: s, s: i, ms: r, Q: f }[t2] || String(t2 || "").toLowerCase().replace(/s$/, "");
|
||||
}, u: function(t2) {
|
||||
return void 0 === t2;
|
||||
} }, g = "en", D = {};
|
||||
D[g] = M;
|
||||
var p = "$isDayjsObject", S = function(t2) {
|
||||
return t2 instanceof _ || !(!t2 || !t2[p]);
|
||||
}, w = function t2(e2, n2, r2) {
|
||||
var i2;
|
||||
if (!e2) return g;
|
||||
if ("string" == typeof e2) {
|
||||
var s2 = e2.toLowerCase();
|
||||
D[s2] && (i2 = s2), n2 && (D[s2] = n2, i2 = s2);
|
||||
var u2 = e2.split("-");
|
||||
if (!i2 && u2.length > 1) return t2(u2[0]);
|
||||
} else {
|
||||
var a2 = e2.name;
|
||||
D[a2] = e2, i2 = a2;
|
||||
}
|
||||
return !r2 && i2 && (g = i2), i2 || !r2 && g;
|
||||
}, O = function(t2, e2) {
|
||||
if (S(t2)) return t2.clone();
|
||||
var n2 = "object" == typeof e2 ? e2 : {};
|
||||
return n2.date = t2, n2.args = arguments, new _(n2);
|
||||
}, b = v;
|
||||
b.l = w, b.i = S, b.w = function(t2, e2) {
|
||||
return O(t2, { locale: e2.$L, utc: e2.$u, x: e2.$x, $offset: e2.$offset });
|
||||
};
|
||||
var _ = (function() {
|
||||
function M2(t2) {
|
||||
this.$L = w(t2.locale, null, true), this.parse(t2), this.$x = this.$x || t2.x || {}, this[p] = true;
|
||||
}
|
||||
var m2 = M2.prototype;
|
||||
return m2.parse = function(t2) {
|
||||
this.$d = (function(t3) {
|
||||
var e2 = t3.date, n2 = t3.utc;
|
||||
if (null === e2) return /* @__PURE__ */ new Date(NaN);
|
||||
if (b.u(e2)) return /* @__PURE__ */ new Date();
|
||||
if (e2 instanceof Date) return new Date(e2);
|
||||
if ("string" == typeof e2 && !/Z$/i.test(e2)) {
|
||||
var r2 = e2.match($);
|
||||
if (r2) {
|
||||
var i2 = r2[2] - 1 || 0, s2 = (r2[7] || "0").substring(0, 3);
|
||||
return n2 ? new Date(Date.UTC(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2)) : new Date(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2);
|
||||
}
|
||||
}
|
||||
return new Date(e2);
|
||||
})(t2), this.init();
|
||||
}, m2.init = function() {
|
||||
var t2 = this.$d;
|
||||
this.$y = t2.getFullYear(), this.$M = t2.getMonth(), this.$D = t2.getDate(), this.$W = t2.getDay(), this.$H = t2.getHours(), this.$m = t2.getMinutes(), this.$s = t2.getSeconds(), this.$ms = t2.getMilliseconds();
|
||||
}, m2.$utils = function() {
|
||||
return b;
|
||||
}, m2.isValid = function() {
|
||||
return !(this.$d.toString() === l);
|
||||
}, m2.isSame = function(t2, e2) {
|
||||
var n2 = O(t2);
|
||||
return this.startOf(e2) <= n2 && n2 <= this.endOf(e2);
|
||||
}, m2.isAfter = function(t2, e2) {
|
||||
return O(t2) < this.startOf(e2);
|
||||
}, m2.isBefore = function(t2, e2) {
|
||||
return this.endOf(e2) < O(t2);
|
||||
}, m2.$g = function(t2, e2, n2) {
|
||||
return b.u(t2) ? this[e2] : this.set(n2, t2);
|
||||
}, m2.unix = function() {
|
||||
return Math.floor(this.valueOf() / 1e3);
|
||||
}, m2.valueOf = function() {
|
||||
return this.$d.getTime();
|
||||
}, m2.startOf = function(t2, e2) {
|
||||
var n2 = this, r2 = !!b.u(e2) || e2, f2 = b.p(t2), l2 = function(t3, e3) {
|
||||
var i2 = b.w(n2.$u ? Date.UTC(n2.$y, e3, t3) : new Date(n2.$y, e3, t3), n2);
|
||||
return r2 ? i2 : i2.endOf(a);
|
||||
}, $2 = function(t3, e3) {
|
||||
return b.w(n2.toDate()[t3].apply(n2.toDate("s"), (r2 ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice(e3)), n2);
|
||||
}, y2 = this.$W, M3 = this.$M, m3 = this.$D, v2 = "set" + (this.$u ? "UTC" : "");
|
||||
switch (f2) {
|
||||
case h:
|
||||
return r2 ? l2(1, 0) : l2(31, 11);
|
||||
case c:
|
||||
return r2 ? l2(1, M3) : l2(0, M3 + 1);
|
||||
case o:
|
||||
var g2 = this.$locale().weekStart || 0, D2 = (y2 < g2 ? y2 + 7 : y2) - g2;
|
||||
return l2(r2 ? m3 - D2 : m3 + (6 - D2), M3);
|
||||
case a:
|
||||
case d:
|
||||
return $2(v2 + "Hours", 0);
|
||||
case u:
|
||||
return $2(v2 + "Minutes", 1);
|
||||
case s:
|
||||
return $2(v2 + "Seconds", 2);
|
||||
case i:
|
||||
return $2(v2 + "Milliseconds", 3);
|
||||
default:
|
||||
return this.clone();
|
||||
}
|
||||
}, m2.endOf = function(t2) {
|
||||
return this.startOf(t2, false);
|
||||
}, m2.$set = function(t2, e2) {
|
||||
var n2, o2 = b.p(t2), f2 = "set" + (this.$u ? "UTC" : ""), l2 = (n2 = {}, n2[a] = f2 + "Date", n2[d] = f2 + "Date", n2[c] = f2 + "Month", n2[h] = f2 + "FullYear", n2[u] = f2 + "Hours", n2[s] = f2 + "Minutes", n2[i] = f2 + "Seconds", n2[r] = f2 + "Milliseconds", n2)[o2], $2 = o2 === a ? this.$D + (e2 - this.$W) : e2;
|
||||
if (o2 === c || o2 === h) {
|
||||
var y2 = this.clone().set(d, 1);
|
||||
y2.$d[l2]($2), y2.init(), this.$d = y2.set(d, Math.min(this.$D, y2.daysInMonth())).$d;
|
||||
} else l2 && this.$d[l2]($2);
|
||||
return this.init(), this;
|
||||
}, m2.set = function(t2, e2) {
|
||||
return this.clone().$set(t2, e2);
|
||||
}, m2.get = function(t2) {
|
||||
return this[b.p(t2)]();
|
||||
}, m2.add = function(r2, f2) {
|
||||
var d2, l2 = this;
|
||||
r2 = Number(r2);
|
||||
var $2 = b.p(f2), y2 = function(t2) {
|
||||
var e2 = O(l2);
|
||||
return b.w(e2.date(e2.date() + Math.round(t2 * r2)), l2);
|
||||
};
|
||||
if ($2 === c) return this.set(c, this.$M + r2);
|
||||
if ($2 === h) return this.set(h, this.$y + r2);
|
||||
if ($2 === a) return y2(1);
|
||||
if ($2 === o) return y2(7);
|
||||
var M3 = (d2 = {}, d2[s] = e, d2[u] = n, d2[i] = t, d2)[$2] || 1, m3 = this.$d.getTime() + r2 * M3;
|
||||
return b.w(m3, this);
|
||||
}, m2.subtract = function(t2, e2) {
|
||||
return this.add(-1 * t2, e2);
|
||||
}, m2.format = function(t2) {
|
||||
var e2 = this, n2 = this.$locale();
|
||||
if (!this.isValid()) return n2.invalidDate || l;
|
||||
var r2 = t2 || "YYYY-MM-DDTHH:mm:ssZ", i2 = b.z(this), s2 = this.$H, u2 = this.$m, a2 = this.$M, o2 = n2.weekdays, c2 = n2.months, f2 = n2.meridiem, h2 = function(t3, n3, i3, s3) {
|
||||
return t3 && (t3[n3] || t3(e2, r2)) || i3[n3].slice(0, s3);
|
||||
}, d2 = function(t3) {
|
||||
return b.s(s2 % 12 || 12, t3, "0");
|
||||
}, $2 = f2 || function(t3, e3, n3) {
|
||||
var r3 = t3 < 12 ? "AM" : "PM";
|
||||
return n3 ? r3.toLowerCase() : r3;
|
||||
};
|
||||
return r2.replace(y, (function(t3, r3) {
|
||||
return r3 || (function(t4) {
|
||||
switch (t4) {
|
||||
case "YY":
|
||||
return String(e2.$y).slice(-2);
|
||||
case "YYYY":
|
||||
return b.s(e2.$y, 4, "0");
|
||||
case "M":
|
||||
return a2 + 1;
|
||||
case "MM":
|
||||
return b.s(a2 + 1, 2, "0");
|
||||
case "MMM":
|
||||
return h2(n2.monthsShort, a2, c2, 3);
|
||||
case "MMMM":
|
||||
return h2(c2, a2);
|
||||
case "D":
|
||||
return e2.$D;
|
||||
case "DD":
|
||||
return b.s(e2.$D, 2, "0");
|
||||
case "d":
|
||||
return String(e2.$W);
|
||||
case "dd":
|
||||
return h2(n2.weekdaysMin, e2.$W, o2, 2);
|
||||
case "ddd":
|
||||
return h2(n2.weekdaysShort, e2.$W, o2, 3);
|
||||
case "dddd":
|
||||
return o2[e2.$W];
|
||||
case "H":
|
||||
return String(s2);
|
||||
case "HH":
|
||||
return b.s(s2, 2, "0");
|
||||
case "h":
|
||||
return d2(1);
|
||||
case "hh":
|
||||
return d2(2);
|
||||
case "a":
|
||||
return $2(s2, u2, true);
|
||||
case "A":
|
||||
return $2(s2, u2, false);
|
||||
case "m":
|
||||
return String(u2);
|
||||
case "mm":
|
||||
return b.s(u2, 2, "0");
|
||||
case "s":
|
||||
return String(e2.$s);
|
||||
case "ss":
|
||||
return b.s(e2.$s, 2, "0");
|
||||
case "SSS":
|
||||
return b.s(e2.$ms, 3, "0");
|
||||
case "Z":
|
||||
return i2;
|
||||
}
|
||||
return null;
|
||||
})(t3) || i2.replace(":", "");
|
||||
}));
|
||||
}, m2.utcOffset = function() {
|
||||
return 15 * -Math.round(this.$d.getTimezoneOffset() / 15);
|
||||
}, m2.diff = function(r2, d2, l2) {
|
||||
var $2, y2 = this, M3 = b.p(d2), m3 = O(r2), v2 = (m3.utcOffset() - this.utcOffset()) * e, g2 = this - m3, D2 = function() {
|
||||
return b.m(y2, m3);
|
||||
};
|
||||
switch (M3) {
|
||||
case h:
|
||||
$2 = D2() / 12;
|
||||
break;
|
||||
case c:
|
||||
$2 = D2();
|
||||
break;
|
||||
case f:
|
||||
$2 = D2() / 3;
|
||||
break;
|
||||
case o:
|
||||
$2 = (g2 - v2) / 6048e5;
|
||||
break;
|
||||
case a:
|
||||
$2 = (g2 - v2) / 864e5;
|
||||
break;
|
||||
case u:
|
||||
$2 = g2 / n;
|
||||
break;
|
||||
case s:
|
||||
$2 = g2 / e;
|
||||
break;
|
||||
case i:
|
||||
$2 = g2 / t;
|
||||
break;
|
||||
default:
|
||||
$2 = g2;
|
||||
}
|
||||
return l2 ? $2 : b.a($2);
|
||||
}, m2.daysInMonth = function() {
|
||||
return this.endOf(c).$D;
|
||||
}, m2.$locale = function() {
|
||||
return D[this.$L];
|
||||
}, m2.locale = function(t2, e2) {
|
||||
if (!t2) return this.$L;
|
||||
var n2 = this.clone(), r2 = w(t2, e2, true);
|
||||
return r2 && (n2.$L = r2), n2;
|
||||
}, m2.clone = function() {
|
||||
return b.w(this.$d, this);
|
||||
}, m2.toDate = function() {
|
||||
return new Date(this.valueOf());
|
||||
}, m2.toJSON = function() {
|
||||
return this.isValid() ? this.toISOString() : null;
|
||||
}, m2.toISOString = function() {
|
||||
return this.$d.toISOString();
|
||||
}, m2.toString = function() {
|
||||
return this.$d.toUTCString();
|
||||
}, M2;
|
||||
})(), k = _.prototype;
|
||||
return O.prototype = k, [["$ms", r], ["$s", i], ["$m", s], ["$H", u], ["$W", a], ["$M", c], ["$y", h], ["$D", d]].forEach((function(t2) {
|
||||
k[t2[1]] = function(e2) {
|
||||
return this.$g(e2, t2[0], t2[1]);
|
||||
};
|
||||
})), O.extend = function(t2, e2) {
|
||||
return t2.$i || (t2(e2, _, O), t2.$i = true), O;
|
||||
}, O.locale = w, O.isDayjs = S, O.unix = function(t2) {
|
||||
return O(1e3 * t2);
|
||||
}, O.en = D[g], O.Ls = D, O.p = {}, O;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
require_dayjs_min
|
||||
};
|
||||
//# sourceMappingURL=chunk-XCUFKJYR.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,20 +0,0 @@
|
||||
// node_modules/ant-design-vue/es/vc-pagination/locale/zh_CN.js
|
||||
var zh_CN_default = {
|
||||
// Options.jsx
|
||||
items_per_page: "条/页",
|
||||
jump_to: "跳至",
|
||||
jump_to_confirm: "确定",
|
||||
page: "页",
|
||||
// Pagination.jsx
|
||||
prev_page: "上一页",
|
||||
next_page: "下一页",
|
||||
prev_5: "向前 5 页",
|
||||
next_5: "向后 5 页",
|
||||
prev_3: "向前 3 页",
|
||||
next_3: "向后 3 页"
|
||||
};
|
||||
|
||||
export {
|
||||
zh_CN_default
|
||||
};
|
||||
//# sourceMappingURL=chunk-YODRZMZT.js.map
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../node_modules/ant-design-vue/es/vc-pagination/locale/zh_CN.js"],
|
||||
"sourcesContent": ["export default {\n // Options.jsx\n items_per_page: '条/页',\n jump_to: '跳至',\n jump_to_confirm: '确定',\n page: '页',\n // Pagination.jsx\n prev_page: '上一页',\n next_page: '下一页',\n prev_5: '向前 5 页',\n next_5: '向后 5 页',\n prev_3: '向前 3 页',\n next_3: '向后 3 页'\n};"],
|
||||
"mappings": ";AAAA,IAAO,gBAAQ;AAAA;AAAA,EAEb,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,MAAM;AAAA;AAAA,EAEN,WAAW;AAAA,EACX,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AACV;",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
|
||||
import {
|
||||
require_dayjs_min
|
||||
} from "./chunk-XCUFKJYR.js";
|
||||
import "./chunk-PR4QN5HX.js";
|
||||
export default require_dayjs_min();
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,343 +0,0 @@
|
||||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-Y7TKRIWE.js";
|
||||
import "./chunk-PR4QN5HX.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>通用大模型模板 · Hertz Admin + AI</h1>
|
||||
|
||||
现代化、可即用的管理后台前端模板。聚焦“工程化 + 体验”,内置账号体系、权限路由、主题美化、知识库、YOLO 模型全流程(管理/类别/告警/历史)等典型模块。
|
||||
|
||||
<p>
|
||||
基于 Vite + Vue 3 + TypeScript + Ant Design Vue + Pinia + Vue Router 构建
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ 特性一览
|
||||
|
||||
- 设计统一:全局“苹果风格”主题(卡片/弹窗/按钮/输入/分页),开箱即用且风格一致
|
||||
- 工程规范:TypeScript 强类型、请求与错误拦截、模块化 API、权限化菜单/路由
|
||||
- 典型业务:
|
||||
- 知识库管理:分类树、列表搜索、编辑/发布,已优化分类切换闪烁
|
||||
- YOLO 模型:模型管理、模型类别管理、告警处理中心、检测历史管理
|
||||
- 认证体系:登录/注册(字段对齐、错误信息透出),可扩展验证码
|
||||
- 体验友好:延迟 loading 避免闪烁、毛玻璃质感、统一按钮与交互反馈
|
||||
|
||||
## 🧩 技术栈
|
||||
|
||||
- 构建:Vite
|
||||
- 语言:TypeScript
|
||||
- 框架:Vue 3(Composition API)
|
||||
- UI:Ant Design Vue
|
||||
- 状态:Pinia
|
||||
- 路由:Vue Router
|
||||
|
||||
## 📦 目录结构(核心)
|
||||
|
||||
```
|
||||
通用大模型模板/
|
||||
└─ hertz_server_diango_ui_2/ # 前端工程(Vite)
|
||||
├─ public/ # 公共静态资源
|
||||
├─ src/
|
||||
│ ├─ api/ # 接口定义(auth、yolo、knowledge、…)
|
||||
│ ├─ locales/ # 国际化
|
||||
│ ├─ router/ # 路由与菜单(admin_menu.ts 自动化)
|
||||
│ ├─ stores/ # Pinia
|
||||
│ ├─ styles/ # 全局样式与变量(index.scss、variables.scss)
|
||||
│ ├─ utils/ # 工具(请求、权限、URL 等)
|
||||
│ └─ views/ # 页面
|
||||
│ ├─ admin_page/ # 管理端模块
|
||||
│ ├─ user_pages/ # 用户端模块
|
||||
│ └─ register.vue / Login.vue # 登录注册
|
||||
├─ index.html
|
||||
├─ package.json
|
||||
└─ vite.config.ts
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
```bash
|
||||
# 进入工程目录
|
||||
cd hertz_server_diango_ui_2
|
||||
|
||||
# 安装依赖
|
||||
npm i
|
||||
|
||||
# 开发启动
|
||||
npm run dev
|
||||
|
||||
|
||||
## ⚙️ 环境与请求
|
||||
|
||||
- 默认使用同域代理或反向代理。按需要在环境文件中设置:
|
||||
```bash
|
||||
# .env.development(示例)
|
||||
VITE_API_BASE=/api
|
||||
```
|
||||
- 请求封装:`src/utils/hertz_request.ts`(拦截器、错误提示、统一 header)
|
||||
|
||||
⚠️注:所有后端IP都更改为“localhost:3000”,需根据具体项目与对应后端开发对接
|
||||
|
||||
## 🔧 关键模块说明
|
||||
|
||||
- 主题美化(Design System)
|
||||
- `src/styles/index.scss`、`src/styles/variables.scss`
|
||||
- 全局统一:Modal/Drawer/Button/Input/Select/Table/Pagination…
|
||||
- 专门处理闪烁与焦点态的视觉细节
|
||||
|
||||
- 菜单与路由
|
||||
- `src/router/admin_menu.ts`:单文件维护菜单与路由,支持权限过滤与自动生成
|
||||
- 统一面包屑:已移除“首页/”的冗余展示,仅保留当前层级
|
||||
|
||||
- 知识库管理
|
||||
- `src/views/admin_page/KnowledgeBaseManagement.vue`
|
||||
- 分类树 + 列表搜索 + 编辑/发布
|
||||
- 已优化分类切换闪烁(分类卡片不 Loading、表格 Loading 延迟)
|
||||
|
||||
- YOLO 模块
|
||||
- 模型管理:上传/列表/启用禁用(苹果风格拖拽区与卡片)
|
||||
- 模型类别管理:别名编辑、等级切换
|
||||
- 告警处理中心:统计卡片、筛选、批量处理、详情预览
|
||||
- 检测历史管理:搜索、筛选、对比查看(图片/视频),已移除“下载结果”按钮(后端未实现)
|
||||
|
||||
- 认证模块
|
||||
- API:`src/api/auth.ts`
|
||||
- 注册页:`src/views/register.vue` 已与后端对齐字段
|
||||
- 提交字段:`username, password, confirm_password, email, phone, real_name`
|
||||
- 兼容字段:`captcha, captcha_id`(未启用可传空串)
|
||||
- 统一错误提示透出
|
||||
|
||||
## 🧪 常见问题(FAQ)
|
||||
|
||||
- 按钮样式与其他页面不一致?
|
||||
- 已在 `src/styles/index.scss` 对 `.ant-btn` 全局统一。若仍不一致,检查局部覆盖或第三方样式。
|
||||
|
||||
- 分类切换时闪烁?
|
||||
- 左侧分类卡片不再受列表 Loading 影响;表格 Loading 使用 `{ spinning, delay: 200 }`。仍抖动可增加骨架屏或请求防抖。
|
||||
|
||||
- 接口没有请求或字段不匹配?
|
||||
- 检查 `src/api/*.ts` 与页面 `payload` 是否一致;打开浏览器 Network 面板查看请求/响应详情。
|
||||
|
||||
## 🛠️ 二次开发建议
|
||||
|
||||
- 新增模块:在 `src/views/admin_page/` 增加页面,并在 `src/router/admin_menu.ts` 添加菜单与路由映射
|
||||
- 改造主题:在 `styles/variables.scss` 修改色板与圆角/阴影;在 `index.scss` 扩展组件级风格
|
||||
- 对接后端:在 `src/api/` 创建对应接口文件,使用统一 `request` 封装
|
||||
|
||||
## 📜 脚本列表
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
80
hertz_server_diango_ui/components.d.ts
vendored
80
hertz_server_diango_ui/components.d.ts
vendored
@@ -1,80 +0,0 @@
|
||||
/* 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 {
|
||||
AAlert: typeof import('ant-design-vue/es')['Alert']
|
||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACard: typeof import('ant-design-vue/es')['Card']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
|
||||
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
|
||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/es')['Image']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
|
||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||
AResult: typeof import('ant-design-vue/es')['Result']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
AStatistic: typeof import('ant-design-vue/es')['Statistic']
|
||||
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
|
||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATimeline: typeof import('ant-design-vue/es')['Timeline']
|
||||
ATimelineItem: typeof import('ant-design-vue/es')['TimelineItem']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
ATree: typeof import('ant-design-vue/es')['Tree']
|
||||
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
// JavaScript 推荐规则
|
||||
js.configs.recommended,
|
||||
|
||||
// TypeScript 推荐规则
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// Vue 推荐规则
|
||||
...vue.configs['flat/recommended'],
|
||||
|
||||
// 项目特定配置
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,vue}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
// 浏览器环境
|
||||
console: 'readonly',
|
||||
window: 'readonly',
|
||||
document: 'readonly',
|
||||
navigator: 'readonly',
|
||||
fetch: 'readonly',
|
||||
// Node.js环境
|
||||
process: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
module: 'readonly',
|
||||
require: 'readonly',
|
||||
global: 'readonly',
|
||||
// Vite环境
|
||||
import: 'readonly',
|
||||
// Vue环境
|
||||
Vue: 'readonly',
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// 禁用所有可能导致WebStorm警告的规则
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-debugger': 'off',
|
||||
'no-alert': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
|
||||
// Vue相关规则
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-unused-vars': 'off',
|
||||
'vue/no-unused-components': 'off',
|
||||
'vue/no-unused-properties': 'off',
|
||||
'vue/require-v-for-key': 'off',
|
||||
'vue/no-use-v-if-with-v-for': 'off',
|
||||
|
||||
// TypeScript规则
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/prefer-const': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
}
|
||||
},
|
||||
|
||||
// 忽略文件
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
'.git/**',
|
||||
'coverage/**',
|
||||
'*.config.js',
|
||||
'*.config.ts',
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理系统模板</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
5692
hertz_server_diango_ui/package-lock.json
generated
5692
hertz_server_diango_ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "hertz_server_django_ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^24.5.2",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"axios": "^1.12.2",
|
||||
"echarts": "^6.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
"pinia": "^3.0.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.5.21",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"daisyui": "^5.1.13",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"sass-embedded": "^1.93.0",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.6",
|
||||
"vue-tsc": "^3.0.7"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,85 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useUserStore } from './stores/hertz_user'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { ConfigProvider } from 'ant-design-vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import enUS from 'ant-design-vue/es/locale/en_US'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 主题配置 - 简约现代风格
|
||||
const theme = ref({
|
||||
algorithm: 'default' as 'default' | 'dark' | 'compact',
|
||||
token: {
|
||||
colorPrimary: '#2563eb',
|
||||
colorSuccess: '#10b981',
|
||||
colorWarning: '#f59e0b',
|
||||
colorError: '#ef4444',
|
||||
borderRadius: 8,
|
||||
fontSize: 14,
|
||||
},
|
||||
})
|
||||
|
||||
// 语言配置
|
||||
const locale = ref(zhCN)
|
||||
|
||||
// 主题切换
|
||||
const toggleTheme = () => {
|
||||
const currentTheme = localStorage.getItem('theme') || 'light'
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light'
|
||||
|
||||
localStorage.setItem('theme', newTheme)
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
theme.value.algorithm = 'dark'
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
theme.value.algorithm = 'default'
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主题
|
||||
onMounted(() => {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light'
|
||||
if (savedTheme === 'dark') {
|
||||
theme.value.algorithm = 'dark'
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
theme.value.algorithm = 'default'
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const showLayout = computed(() => {
|
||||
return userStore.isLoggedIn
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<ConfigProvider :theme="theme" :locale="locale">
|
||||
<RouterView />
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,96 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 会话与消息类型
|
||||
export interface AIChatItem {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
latest_message?: string
|
||||
}
|
||||
|
||||
export interface AIChatDetail {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AIChatMessage {
|
||||
id: number
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ChatListData {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
chats: AIChatItem[]
|
||||
}
|
||||
|
||||
export interface ChatDetailData {
|
||||
chat: AIChatDetail
|
||||
messages: AIChatMessage[]
|
||||
}
|
||||
|
||||
export interface SendMessageData {
|
||||
user_message: AIChatMessage
|
||||
ai_message: AIChatMessage
|
||||
}
|
||||
|
||||
// 将后端可能返回的 chat_id 统一规范为 id
|
||||
const normalizeChatItem = (raw: any): AIChatItem => ({
|
||||
id: typeof raw?.id === 'number' ? raw.id : Number(raw?.chat_id),
|
||||
title: raw?.title,
|
||||
created_at: raw?.created_at,
|
||||
updated_at: raw?.updated_at,
|
||||
latest_message: raw?.latest_message,
|
||||
})
|
||||
|
||||
const normalizeChatDetail = (raw: any): AIChatDetail => ({
|
||||
id: typeof raw?.id === 'number' ? raw.id : Number(raw?.chat_id),
|
||||
title: raw?.title,
|
||||
created_at: raw?.created_at,
|
||||
updated_at: raw?.updated_at,
|
||||
})
|
||||
|
||||
export const aiApi = {
|
||||
listChats: (params?: { query?: string; page?: number; page_size?: number }): Promise<ApiResponse<ChatListData>> =>
|
||||
request.get('/api/ai/chats/', { params, showError: false }).then((resp: any) => {
|
||||
if (resp?.data?.chats && Array.isArray(resp.data.chats)) {
|
||||
resp.data.chats = resp.data.chats.map((c: any) => normalizeChatItem(c))
|
||||
}
|
||||
return resp as ApiResponse<ChatListData>
|
||||
}),
|
||||
|
||||
createChat: (body?: { title?: string }): Promise<ApiResponse<AIChatDetail>> =>
|
||||
request.post('/api/ai/chats/create/', body || { title: '新对话' }).then((resp: any) => {
|
||||
if (resp?.data) resp.data = normalizeChatDetail(resp.data)
|
||||
return resp as ApiResponse<AIChatDetail>
|
||||
}),
|
||||
|
||||
getChatDetail: (chatId: number): Promise<ApiResponse<ChatDetailData>> =>
|
||||
request.get(`/api/ai/chats/${chatId}/`).then((resp: any) => {
|
||||
if (resp?.data?.chat) resp.data.chat = normalizeChatDetail(resp.data.chat)
|
||||
return resp as ApiResponse<ChatDetailData>
|
||||
}),
|
||||
|
||||
updateChat: (chatId: number, body: { title: string }): Promise<ApiResponse<null>> =>
|
||||
request.put(`/api/ai/chats/${chatId}/update/`, body),
|
||||
|
||||
deleteChats: (chatIds: number[]): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/ai/chats/delete/', { chat_ids: chatIds }),
|
||||
|
||||
sendMessage: (chatId: number, body: { content: string }): Promise<ApiResponse<SendMessageData>> =>
|
||||
request.post(`/api/ai/chats/${chatId}/send/`, body),
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 注册接口数据类型
|
||||
export interface RegisterData {
|
||||
username: string
|
||||
password: string
|
||||
confirm_password: string
|
||||
email: string
|
||||
phone: string
|
||||
real_name: string
|
||||
captcha: string
|
||||
captcha_id: string
|
||||
}
|
||||
|
||||
// 发送邮箱验证码数据类型
|
||||
export interface SendEmailCodeData {
|
||||
email: string
|
||||
code_type: string
|
||||
}
|
||||
|
||||
// 登录接口数据类型
|
||||
export interface LoginData {
|
||||
username: string
|
||||
password: string
|
||||
captcha_code: string
|
||||
captcha_key: string
|
||||
}
|
||||
|
||||
// 注册API
|
||||
export const registerUser = (data: RegisterData) => {
|
||||
return request.post('/api/auth/register/', data)
|
||||
}
|
||||
|
||||
// 登录API
|
||||
export const loginUser = (data: LoginData) => {
|
||||
return request.post('/api/auth/login/', data)
|
||||
}
|
||||
|
||||
// 发送邮箱验证码API
|
||||
export const sendEmailCode = (data: SendEmailCodeData) => {
|
||||
return request.post('/api/auth/email/code/', data)
|
||||
}
|
||||
|
||||
// 登出API
|
||||
export const logoutUser = () => {
|
||||
return request.post('/api/auth/logout/')
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 验证码相关接口类型定义
|
||||
export interface CaptchaResponse {
|
||||
captcha_id: string
|
||||
image_data: string // base64编码的图片
|
||||
expires_in: number // 过期时间(秒)
|
||||
}
|
||||
|
||||
export interface CaptchaRefreshResponse {
|
||||
captcha_id: string
|
||||
image_data: string // base64编码的图片
|
||||
expires_in: number // 过期时间(秒)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
export const generateCaptcha = async (): Promise<CaptchaResponse> => {
|
||||
console.log('🚀 开始发送验证码生成请求...')
|
||||
console.log('📍 请求URL:', `${import.meta.env.VITE_API_BASE_URL}/api/captcha/generate/`)
|
||||
console.log('🌐 环境变量 VITE_API_BASE_URL:', import.meta.env.VITE_API_BASE_URL)
|
||||
|
||||
try {
|
||||
const response = await request.post<{
|
||||
code: number
|
||||
message: string
|
||||
data: CaptchaResponse
|
||||
}>('/api/captcha/generate/')
|
||||
|
||||
console.log('✅ 验证码生成请求成功:', response)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('❌ 验证码生成请求失败 - 完整错误信息:')
|
||||
console.error('错误对象:', error)
|
||||
console.error('错误类型:', typeof error)
|
||||
console.error('错误消息:', error?.message)
|
||||
console.error('错误代码:', error?.code)
|
||||
console.error('错误状态:', error?.status)
|
||||
console.error('错误响应:', error?.response)
|
||||
console.error('错误请求:', error?.request)
|
||||
console.error('错误配置:', error?.config)
|
||||
|
||||
// 检查是否是网络错误
|
||||
if (error?.code === 'NETWORK_ERROR' || error?.message?.includes('Network Error')) {
|
||||
console.error('🌐 网络连接错误 - 可能的原因:')
|
||||
console.error('1. 后端服务器未启动')
|
||||
console.error('2. API地址不正确')
|
||||
console.error('3. CORS配置问题')
|
||||
console.error('4. 防火墙阻止连接')
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新验证码
|
||||
*/
|
||||
export const refreshCaptcha = async (captcha_id: string): Promise<CaptchaRefreshResponse> => {
|
||||
console.log('🔄 开始发送验证码刷新请求...')
|
||||
console.log('📍 请求URL:', `${import.meta.env.VITE_API_BASE_URL}/api/captcha/refresh/`)
|
||||
console.log('📦 请求数据:', { captcha_id })
|
||||
|
||||
try {
|
||||
const response = await request.post<{
|
||||
code: number
|
||||
message: string
|
||||
data: CaptchaRefreshResponse
|
||||
}>('/api/captcha/refresh/', {
|
||||
captcha_id
|
||||
})
|
||||
|
||||
console.log('✅ 验证码刷新请求成功:', response)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('❌ 验证码刷新请求失败 - 完整错误信息:')
|
||||
console.error('错误对象:', error)
|
||||
console.error('错误类型:', typeof error)
|
||||
console.error('错误消息:', error?.message)
|
||||
console.error('错误代码:', error?.code)
|
||||
console.error('错误状态:', error?.status)
|
||||
console.error('错误响应:', error?.response)
|
||||
console.error('错误请求:', error?.request)
|
||||
console.error('错误配置:', error?.config)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
import { logApi, type OperationLogListItem } from './log'
|
||||
import { systemMonitorApi, type SystemInfo, type CpuInfo, type MemoryInfo, type DiskInfo } from './system_monitor'
|
||||
import { noticeUserApi } from './notice_user'
|
||||
import { knowledgeApi } from './knowledge'
|
||||
|
||||
// 仪表盘统计数据类型定义
|
||||
export interface DashboardStats {
|
||||
totalUsers: number
|
||||
totalNotifications: number
|
||||
totalLogs: number
|
||||
totalKnowledge: number
|
||||
userGrowthRate: number
|
||||
notificationGrowthRate: number
|
||||
logGrowthRate: number
|
||||
knowledgeGrowthRate: number
|
||||
}
|
||||
|
||||
// 最近活动数据类型
|
||||
export interface RecentActivity {
|
||||
id: number
|
||||
action: string
|
||||
time: string
|
||||
user: string
|
||||
type: 'login' | 'create' | 'update' | 'system' | 'register'
|
||||
}
|
||||
|
||||
// 系统状态数据类型
|
||||
export interface SystemStatus {
|
||||
cpuUsage: number
|
||||
memoryUsage: number
|
||||
diskUsage: number
|
||||
networkStatus: 'normal' | 'warning' | 'error'
|
||||
}
|
||||
|
||||
// 访问趋势数据类型
|
||||
export interface VisitTrend {
|
||||
date: string
|
||||
visits: number
|
||||
users: number
|
||||
}
|
||||
|
||||
// 仪表盘数据汇总类型
|
||||
export interface DashboardData {
|
||||
stats: DashboardStats
|
||||
recentActivities: RecentActivity[]
|
||||
systemStatus: SystemStatus
|
||||
visitTrends: VisitTrend[]
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 仪表盘API接口
|
||||
export const dashboardApi = {
|
||||
// 获取仪表盘统计数据
|
||||
getStats: (): Promise<ApiResponse<DashboardStats>> => {
|
||||
return request.get('/api/dashboard/stats/')
|
||||
},
|
||||
|
||||
// 获取真实统计数据
|
||||
getRealStats: async (): Promise<ApiResponse<DashboardStats>> => {
|
||||
try {
|
||||
// 并行获取各种统计数据
|
||||
const [notificationStats, logStats, knowledgeStats] = await Promise.all([
|
||||
noticeUserApi.statistics().catch(() => ({ success: false, data: { total_count: 0, unread_count: 0 } })),
|
||||
logApi.getList({ page: 1, page_size: 1 }).catch(() => ({ success: false, data: { count: 0 } })),
|
||||
knowledgeApi.getArticles({ page: 1, page_size: 1 }).catch(() => ({ success: false, data: { total: 0 } }))
|
||||
])
|
||||
|
||||
// 计算统计数据
|
||||
const totalNotifications = notificationStats.success ? (notificationStats.data.total_count || 0) : 0
|
||||
|
||||
// 处理日志数据 - 兼容多种返回结构
|
||||
let totalLogs = 0
|
||||
if (logStats.success && logStats.data) {
|
||||
const logData = logStats.data as any
|
||||
console.log('日志API响应数据:', logData)
|
||||
// 兼容DRF标准结构:{ count, next, previous, results }
|
||||
if ('count' in logData) {
|
||||
totalLogs = Number(logData.count) || 0
|
||||
} else if ('total' in logData) {
|
||||
totalLogs = Number(logData.total) || 0
|
||||
} else if ('total_count' in logData) {
|
||||
totalLogs = Number(logData.total_count) || 0
|
||||
} else if (logData.pagination && logData.pagination.total_count) {
|
||||
totalLogs = Number(logData.pagination.total_count) || 0
|
||||
}
|
||||
console.log('解析出的日志总数:', totalLogs)
|
||||
} else {
|
||||
console.log('日志API调用失败:', logStats)
|
||||
}
|
||||
|
||||
const totalKnowledge = knowledgeStats.success ? (knowledgeStats.data.total || 0) : 0
|
||||
|
||||
console.log('统计数据汇总:', { totalNotifications, totalLogs, totalKnowledge })
|
||||
|
||||
// 模拟增长率(实际项目中应该从后端获取)
|
||||
const stats: DashboardStats = {
|
||||
totalUsers: 0, // 暂时设为0,需要用户管理API
|
||||
totalNotifications,
|
||||
totalLogs,
|
||||
totalKnowledge,
|
||||
userGrowthRate: 0,
|
||||
notificationGrowthRate: Math.floor(Math.random() * 20) - 10, // 模拟 -10% 到 +10%
|
||||
logGrowthRate: Math.floor(Math.random() * 30) - 15, // 模拟 -15% 到 +15%
|
||||
knowledgeGrowthRate: Math.floor(Math.random() * 25) - 12 // 模拟 -12% 到 +13%
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: stats
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取真实统计数据失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取统计数据失败',
|
||||
data: {
|
||||
totalUsers: 0,
|
||||
totalNotifications: 0,
|
||||
totalLogs: 0,
|
||||
totalKnowledge: 0,
|
||||
userGrowthRate: 0,
|
||||
notificationGrowthRate: 0,
|
||||
logGrowthRate: 0,
|
||||
knowledgeGrowthRate: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取最近活动(从日志接口)
|
||||
getRecentActivities: async (limit: number = 10): Promise<ApiResponse<RecentActivity[]>> => {
|
||||
try {
|
||||
const response = await logApi.getList({ page: 1, page_size: limit })
|
||||
if (response.success && response.data) {
|
||||
// 根据实际API响应结构,数据可能在data.logs或data.results中
|
||||
const logs = (response.data as any).logs || (response.data as any).results || []
|
||||
const activities: RecentActivity[] = logs.map((log: any) => ({
|
||||
id: log.log_id || log.id,
|
||||
action: log.description || log.operation_description || `${log.action_type_display || log.operation_type} - ${log.module || log.operation_module}`,
|
||||
time: formatTimeAgo(log.created_at),
|
||||
user: log.username || log.user?.username || '未知用户',
|
||||
type: mapLogTypeToActivityType(log.action_type || log.operation_type)
|
||||
}))
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: activities
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取活动数据失败',
|
||||
data: []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取活动数据失败',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取系统状态(从系统监控接口)
|
||||
getSystemStatus: async (): Promise<ApiResponse<SystemStatus>> => {
|
||||
try {
|
||||
const [cpuResponse, memoryResponse, disksResponse] = await Promise.all([
|
||||
systemMonitorApi.getCpu(),
|
||||
systemMonitorApi.getMemory(),
|
||||
systemMonitorApi.getDisks()
|
||||
])
|
||||
|
||||
if (cpuResponse.success && memoryResponse.success && disksResponse.success) {
|
||||
// 根据实际API响应结构映射数据
|
||||
const systemStatus: SystemStatus = {
|
||||
// CPU使用率:从 cpu_percent 字段获取
|
||||
cpuUsage: Math.round(cpuResponse.data.cpu_percent || 0),
|
||||
// 内存使用率:从 percent 字段获取
|
||||
memoryUsage: Math.round(memoryResponse.data.percent || 0),
|
||||
// 磁盘使用率:从磁盘数组的第一个磁盘的 percent 字段获取
|
||||
diskUsage: disksResponse.data.length > 0 ? Math.round(disksResponse.data[0].percent || 0) : 0,
|
||||
networkStatus: 'normal' as const
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: systemStatus
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取系统状态失败',
|
||||
data: {
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0,
|
||||
networkStatus: 'error' as const
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统状态失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取系统状态失败',
|
||||
data: {
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0,
|
||||
networkStatus: 'error' as const
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取访问趋势
|
||||
getVisitTrends: (period: 'week' | 'month' | 'year' = 'week'): Promise<ApiResponse<VisitTrend[]>> => {
|
||||
return request.get('/api/dashboard/visit-trends/', { params: { period } })
|
||||
},
|
||||
|
||||
// 获取完整仪表盘数据
|
||||
getDashboardData: (): Promise<ApiResponse<DashboardData>> => {
|
||||
return request.get('/api/dashboard/overview/')
|
||||
},
|
||||
|
||||
// 模拟数据方法(用于开发阶段)
|
||||
getMockStats: (): Promise<DashboardStats> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
totalUsers: 1128,
|
||||
todayVisits: 893,
|
||||
totalOrders: 234,
|
||||
totalRevenue: 12560.50,
|
||||
userGrowthRate: 12,
|
||||
visitGrowthRate: 8,
|
||||
orderGrowthRate: -3,
|
||||
revenueGrowthRate: 15
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
|
||||
getMockActivities: (): Promise<RecentActivity[]> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{
|
||||
id: 1,
|
||||
action: '用户 张三 登录了系统',
|
||||
time: '2分钟前',
|
||||
user: '张三',
|
||||
type: 'login'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
action: '管理员 李四 创建了新部门',
|
||||
time: '5分钟前',
|
||||
user: '李四',
|
||||
type: 'create'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
action: '用户 王五 修改了个人信息',
|
||||
time: '10分钟前',
|
||||
user: '王五',
|
||||
type: 'update'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
action: '系统自动备份完成',
|
||||
time: '1小时前',
|
||||
user: '系统',
|
||||
type: 'system'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
action: '新用户 赵六 注册成功',
|
||||
time: '2小时前',
|
||||
user: '赵六',
|
||||
type: 'register'
|
||||
}
|
||||
])
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
|
||||
getMockSystemStatus: (): Promise<SystemStatus> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
cpuUsage: 45,
|
||||
memoryUsage: 67,
|
||||
diskUsage: 32,
|
||||
networkStatus: 'normal'
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
},
|
||||
|
||||
getMockVisitTrends: (period: 'week' | 'month' | 'year' = 'week'): Promise<VisitTrend[]> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const data = {
|
||||
week: [
|
||||
{ date: '周一', visits: 120, users: 80 },
|
||||
{ date: '周二', visits: 150, users: 95 },
|
||||
{ date: '周三', visits: 180, users: 110 },
|
||||
{ date: '周四', visits: 200, users: 130 },
|
||||
{ date: '周五', visits: 250, users: 160 },
|
||||
{ date: '周六', visits: 180, users: 120 },
|
||||
{ date: '周日', visits: 160, users: 100 }
|
||||
],
|
||||
month: [
|
||||
{ date: '第1周', visits: 800, users: 500 },
|
||||
{ date: '第2周', visits: 950, users: 600 },
|
||||
{ date: '第3周', visits: 1100, users: 700 },
|
||||
{ date: '第4周', visits: 1200, users: 750 }
|
||||
],
|
||||
year: [
|
||||
{ date: '1月', visits: 3200, users: 2000 },
|
||||
{ date: '2月', visits: 3800, users: 2400 },
|
||||
{ date: '3月', visits: 4200, users: 2600 },
|
||||
{ date: '4月', visits: 3900, users: 2300 },
|
||||
{ date: '5月', visits: 4500, users: 2800 },
|
||||
{ date: '6月', visits: 5000, users: 3100 }
|
||||
]
|
||||
}
|
||||
resolve(data[period])
|
||||
}, 400)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:格式化时间为相对时间
|
||||
function formatTimeAgo(dateString: string): string {
|
||||
const now = new Date()
|
||||
const date = new Date(dateString)
|
||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return `${diffInSeconds}秒前`
|
||||
} else if (diffInSeconds < 3600) {
|
||||
const minutes = Math.floor(diffInSeconds / 60)
|
||||
return `${minutes}分钟前`
|
||||
} else if (diffInSeconds < 86400) {
|
||||
const hours = Math.floor(diffInSeconds / 3600)
|
||||
return `${hours}小时前`
|
||||
} else {
|
||||
const days = Math.floor(diffInSeconds / 86400)
|
||||
return `${days}天前`
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:将日志操作类型映射为活动类型
|
||||
function mapLogTypeToActivityType(operationType: string): RecentActivity['type'] {
|
||||
if (!operationType) return 'system'
|
||||
|
||||
const lowerType = operationType.toLowerCase()
|
||||
|
||||
if (lowerType.includes('login') || lowerType.includes('登录')) {
|
||||
return 'login'
|
||||
} else if (lowerType.includes('create') || lowerType.includes('创建') || lowerType.includes('add') || lowerType.includes('新增')) {
|
||||
return 'create'
|
||||
} else if (lowerType.includes('update') || lowerType.includes('修改') || lowerType.includes('edit') || lowerType.includes('更新')) {
|
||||
return 'update'
|
||||
} else if (lowerType.includes('register') || lowerType.includes('注册')) {
|
||||
return 'register'
|
||||
} else if (lowerType.includes('view') || lowerType.includes('查看') || lowerType.includes('get') || lowerType.includes('获取')) {
|
||||
return 'system'
|
||||
} else {
|
||||
return 'system'
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 部门数据类型定义
|
||||
export interface Department {
|
||||
dept_id: number
|
||||
parent_id: number | null
|
||||
dept_name: string
|
||||
dept_code: string
|
||||
leader: string
|
||||
phone: string | null
|
||||
email: string | null
|
||||
status: number
|
||||
sort_order: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
children?: Department[]
|
||||
user_count?: number
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 部门列表数据类型
|
||||
export interface DepartmentListData {
|
||||
list: Department[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export type DepartmentListResponse = ApiResponse<DepartmentListData>
|
||||
|
||||
// 部门列表查询参数
|
||||
export interface DepartmentListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
parent_id?: number
|
||||
}
|
||||
|
||||
// 创建部门参数
|
||||
export interface CreateDepartmentParams {
|
||||
parent_id: null
|
||||
dept_name: string
|
||||
dept_code: string
|
||||
leader: string
|
||||
phone: string
|
||||
email: string
|
||||
status: number
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
// 更新部门参数
|
||||
export type UpdateDepartmentParams = Partial<CreateDepartmentParams>
|
||||
|
||||
// 部门API接口
|
||||
export const departmentApi = {
|
||||
// 获取部门列表
|
||||
getDepartmentList: (params?: DepartmentListParams): Promise<ApiResponse<Department[]>> => {
|
||||
return request.get('/api/departments/', { params })
|
||||
},
|
||||
|
||||
// 获取部门详情
|
||||
getDepartment: (id: number): Promise<ApiResponse<Department>> => {
|
||||
return request.get(`/api/departments/${id}/`)
|
||||
},
|
||||
|
||||
// 创建部门
|
||||
createDepartment: (data: CreateDepartmentParams): Promise<ApiResponse<Department>> => {
|
||||
return request.post('/api/departments/create/', data)
|
||||
},
|
||||
|
||||
// 更新部门
|
||||
updateDepartment: (id: number, data: UpdateDepartmentParams): Promise<ApiResponse<Department>> => {
|
||||
return request.put(`/api/departments/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除部门
|
||||
deleteDepartment: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/departments/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 获取部门树
|
||||
getDepartmentTree: (): Promise<ApiResponse<Department[]>> => {
|
||||
return request.get('/api/departments/tree/')
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// API 统一出口文件
|
||||
export * from './captcha'
|
||||
export * from './auth'
|
||||
export * from './user'
|
||||
export * from './department'
|
||||
export * from './menu'
|
||||
export * from './role'
|
||||
export * from './password'
|
||||
export * from './system_monitor'
|
||||
export * from './dashboard'
|
||||
|
||||
export * from './ai'
|
||||
// 这里可以继续添加其它 API 模块的导出,例如:
|
||||
// export * from './admin'
|
||||
export * from './log'
|
||||
export * from './knowledge'
|
||||
@@ -1,173 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 分类类型
|
||||
export interface KnowledgeCategory {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
parent?: number | null
|
||||
parent_name?: string | null
|
||||
sort_order?: number
|
||||
is_active?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
children_count?: number
|
||||
articles_count?: number
|
||||
full_path?: string
|
||||
children?: KnowledgeCategory[]
|
||||
}
|
||||
|
||||
export interface CategoryListData {
|
||||
list: KnowledgeCategory[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface CategoryListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
name?: string
|
||||
parent_id?: number
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
// 文章类型
|
||||
export interface KnowledgeArticleListItem {
|
||||
id: number
|
||||
title: string
|
||||
summary?: string | null
|
||||
image?: string | null
|
||||
category_name: string
|
||||
author_name: string
|
||||
status: 'draft' | 'published' | 'archived'
|
||||
status_display: string
|
||||
view_count?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
published_at?: string | null
|
||||
}
|
||||
|
||||
export interface KnowledgeArticleDetail extends KnowledgeArticleListItem {
|
||||
content: string
|
||||
category: number
|
||||
author: number
|
||||
tags?: string
|
||||
tags_list?: string[]
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
export interface ArticleListData {
|
||||
list: KnowledgeArticleListItem[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface ArticleListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
title?: string
|
||||
category_id?: number
|
||||
author_id?: number
|
||||
status?: 'draft' | 'published' | 'archived'
|
||||
tags?: string
|
||||
}
|
||||
|
||||
export interface CreateArticlePayload {
|
||||
title: string
|
||||
content: string
|
||||
summary?: string
|
||||
image?: string
|
||||
category: number
|
||||
status?: 'draft' | 'published'
|
||||
tags?: string
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
export interface UpdateArticlePayload {
|
||||
title?: string
|
||||
content?: string
|
||||
summary?: string
|
||||
image?: string
|
||||
category?: number
|
||||
status?: 'draft' | 'published' | 'archived'
|
||||
tags?: string
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
// 知识库 API
|
||||
export const knowledgeApi = {
|
||||
// 分类:列表
|
||||
getCategories: (params?: CategoryListParams): Promise<ApiResponse<CategoryListData>> => {
|
||||
return request.get('/api/wiki/categories/', { params })
|
||||
},
|
||||
|
||||
// 分类:树形
|
||||
getCategoryTree: (): Promise<ApiResponse<KnowledgeCategory[]>> => {
|
||||
return request.get('/api/wiki/categories/tree/')
|
||||
},
|
||||
|
||||
// 分类:详情
|
||||
getCategory: (id: number): Promise<ApiResponse<KnowledgeCategory>> => {
|
||||
return request.get(`/api/wiki/categories/${id}/`)
|
||||
},
|
||||
|
||||
// 分类:创建
|
||||
createCategory: (data: Partial<KnowledgeCategory>): Promise<ApiResponse<KnowledgeCategory>> => {
|
||||
return request.post('/api/wiki/categories/create/', data)
|
||||
},
|
||||
|
||||
// 分类:更新
|
||||
updateCategory: (id: number, data: Partial<KnowledgeCategory>): Promise<ApiResponse<KnowledgeCategory>> => {
|
||||
return request.put(`/api/wiki/categories/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 分类:删除
|
||||
deleteCategory: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.delete(`/api/wiki/categories/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 文章:列表
|
||||
getArticles: (params?: ArticleListParams): Promise<ApiResponse<ArticleListData>> => {
|
||||
return request.get('/api/wiki/articles/', { params })
|
||||
},
|
||||
|
||||
// 文章:详情
|
||||
getArticle: (id: number): Promise<ApiResponse<KnowledgeArticleDetail>> => {
|
||||
return request.get(`/api/wiki/articles/${id}/`)
|
||||
},
|
||||
|
||||
// 文章:创建
|
||||
createArticle: (data: CreateArticlePayload): Promise<ApiResponse<KnowledgeArticleDetail>> => {
|
||||
return request.post('/api/wiki/articles/create/', data)
|
||||
},
|
||||
|
||||
// 文章:更新
|
||||
updateArticle: (id: number, data: UpdateArticlePayload): Promise<ApiResponse<KnowledgeArticleDetail>> => {
|
||||
return request.put(`/api/wiki/articles/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 文章:删除
|
||||
deleteArticle: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.delete(`/api/wiki/articles/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 文章:发布
|
||||
publishArticle: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.post(`/api/wiki/articles/${id}/publish/`)
|
||||
},
|
||||
|
||||
// 文章:归档
|
||||
archiveArticle: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.post(`/api/wiki/articles/${id}/archive/`)
|
||||
},
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用 API 响应结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 列表查询参数
|
||||
export interface LogListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
user_id?: number
|
||||
operation_type?: string
|
||||
operation_module?: string
|
||||
start_date?: string // YYYY-MM-DD
|
||||
end_date?: string // YYYY-MM-DD
|
||||
ip_address?: string
|
||||
status?: number
|
||||
// 新增:按请求方法与路径、关键字筛选(与后端保持可选兼容)
|
||||
request_method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | string
|
||||
request_path?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
// 列表项(精简字段)
|
||||
export interface OperationLogItem {
|
||||
id: number
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
email?: string
|
||||
} | null
|
||||
operation_type: string
|
||||
// 展示字段
|
||||
action_type_display?: string
|
||||
operation_module: string
|
||||
operation_description?: string
|
||||
target_model?: string
|
||||
target_object_id?: string
|
||||
ip_address?: string
|
||||
request_method: string
|
||||
request_path: string
|
||||
response_status: number
|
||||
// 结果与状态展示
|
||||
status_display?: string
|
||||
is_success?: boolean
|
||||
execution_time?: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 列表响应 data 结构
|
||||
export interface LogListData {
|
||||
count: number
|
||||
next: string | null
|
||||
previous: string | null
|
||||
results: OperationLogItem[]
|
||||
}
|
||||
|
||||
export type LogListResponse = ApiResponse<LogListData>
|
||||
|
||||
// 详情数据(完整字段)
|
||||
export interface OperationLogDetail {
|
||||
id: number
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
email?: string
|
||||
} | null
|
||||
operation_type: string
|
||||
action_type_display?: string
|
||||
operation_module: string
|
||||
operation_description: string
|
||||
target_model?: string
|
||||
target_object_id?: string
|
||||
ip_address?: string
|
||||
user_agent?: string
|
||||
request_method: string
|
||||
request_path: string
|
||||
request_data?: Record<string, any>
|
||||
response_status: number
|
||||
status_display?: string
|
||||
is_success?: boolean
|
||||
response_data?: Record<string, any>
|
||||
execution_time?: number
|
||||
created_at: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export type LogDetailResponse = ApiResponse<OperationLogDetail>
|
||||
|
||||
export const logApi = {
|
||||
// 获取操作日志列表
|
||||
getList: (params: LogListParams, options?: { signal?: AbortSignal }): Promise<LogListResponse> => {
|
||||
// 关闭统一错误弹窗,由页面自行处理
|
||||
return request.get('/api/log/list/', { params, showError: false, signal: options?.signal })
|
||||
},
|
||||
|
||||
// 获取操作日志详情
|
||||
getDetail: (logId: number): Promise<LogDetailResponse> => {
|
||||
return request.get(`/api/log/detail/${logId}/`)
|
||||
},
|
||||
|
||||
// 兼容查询参数方式的详情(部分后端实现为 /api/log/detail/?id=xx 或 ?log_id=xx)
|
||||
getDetailByQuery: (logId: number): Promise<LogDetailResponse> => {
|
||||
return request.get('/api/log/detail/', { params: { id: logId, log_id: logId } })
|
||||
},
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 后端返回的原始菜单数据格式
|
||||
export interface RawMenu {
|
||||
menu_id: number
|
||||
menu_name: string
|
||||
menu_code: string
|
||||
menu_type: number // 后端返回数字:1=菜单, 2=按钮, 3=接口
|
||||
parent_id?: number | null
|
||||
path?: string
|
||||
component?: string | null
|
||||
icon?: string
|
||||
permission?: string
|
||||
sort_order?: number
|
||||
description?: string
|
||||
status?: number
|
||||
is_external?: boolean
|
||||
is_cache?: boolean
|
||||
is_visible?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
children?: RawMenu[]
|
||||
}
|
||||
|
||||
// 前端使用的菜单接口类型定义
|
||||
export interface Menu {
|
||||
menu_id: number
|
||||
menu_name: string
|
||||
menu_code: string
|
||||
menu_type: number // 1=菜单, 2=按钮, 3=接口
|
||||
parent_id?: number
|
||||
path?: string
|
||||
component?: string
|
||||
icon?: string
|
||||
permission?: string
|
||||
sort_order?: number
|
||||
status?: number
|
||||
is_external?: boolean
|
||||
is_cache?: boolean
|
||||
is_visible?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
children?: Menu[]
|
||||
}
|
||||
|
||||
// API响应基础结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 菜单列表数据结构
|
||||
export interface MenuListData {
|
||||
list: Menu[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 菜单列表响应类型
|
||||
export type MenuListResponse = ApiResponse<MenuListData>
|
||||
|
||||
// 菜单列表查询参数
|
||||
export interface MenuListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
menu_type?: string
|
||||
parent_id?: number
|
||||
}
|
||||
|
||||
// 创建菜单参数
|
||||
export interface CreateMenuParams {
|
||||
menu_name: string
|
||||
menu_code: string
|
||||
menu_type: number // 1=菜单, 2=按钮, 3=接口
|
||||
parent_id?: number
|
||||
path?: string
|
||||
component?: string
|
||||
icon?: string
|
||||
permission?: string
|
||||
sort_order?: number
|
||||
status?: number
|
||||
is_external?: boolean
|
||||
is_cache?: boolean
|
||||
is_visible?: boolean
|
||||
}
|
||||
|
||||
// 更新菜单参数
|
||||
export type UpdateMenuParams = Partial<CreateMenuParams>
|
||||
|
||||
// 菜单树响应类型
|
||||
export type MenuTreeResponse = ApiResponse<Menu[]>
|
||||
|
||||
// 数据转换工具函数
|
||||
const convertMenuType = (type: number): 'menu' | 'button' | 'api' => {
|
||||
switch (type) {
|
||||
case 1: return 'menu'
|
||||
case 2: return 'button'
|
||||
case 3: return 'api'
|
||||
default: return 'menu'
|
||||
}
|
||||
}
|
||||
|
||||
// 解码Unicode字符串
|
||||
const decodeUnicode = (str: string): string => {
|
||||
try {
|
||||
return str.replace(/\\u[\dA-F]{4}/gi, (match) => {
|
||||
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16))
|
||||
})
|
||||
} catch (error) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
// 转换原始菜单数据为前端格式
|
||||
const transformRawMenu = (rawMenu: RawMenu): Menu => {
|
||||
// 确保status字段被正确转换
|
||||
let statusValue: number
|
||||
if (rawMenu.status === undefined || rawMenu.status === null) {
|
||||
// 如果status缺失,默认为启用(1)
|
||||
statusValue = 1
|
||||
} else {
|
||||
// 如果有值,转换为数字
|
||||
if (typeof rawMenu.status === 'string') {
|
||||
const parsed = parseInt(rawMenu.status, 10)
|
||||
statusValue = isNaN(parsed) ? 1 : parsed
|
||||
} else {
|
||||
statusValue = Number(rawMenu.status)
|
||||
// 如果转换失败,默认为启用
|
||||
if (isNaN(statusValue)) {
|
||||
statusValue = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menu_id: rawMenu.menu_id,
|
||||
menu_name: decodeUnicode(rawMenu.menu_name),
|
||||
menu_code: rawMenu.menu_code,
|
||||
menu_type: rawMenu.menu_type,
|
||||
parent_id: rawMenu.parent_id || undefined,
|
||||
path: rawMenu.path,
|
||||
component: rawMenu.component,
|
||||
icon: rawMenu.icon,
|
||||
permission: rawMenu.permission,
|
||||
sort_order: rawMenu.sort_order,
|
||||
status: statusValue, // 使用转换后的值
|
||||
is_external: rawMenu.is_external,
|
||||
is_cache: rawMenu.is_cache,
|
||||
is_visible: rawMenu.is_visible,
|
||||
created_at: rawMenu.created_at,
|
||||
updated_at: rawMenu.updated_at,
|
||||
children: rawMenu.children ? rawMenu.children.map(transformRawMenu) : []
|
||||
}
|
||||
}
|
||||
|
||||
// 将菜单数据数组转换为列表格式
|
||||
const transformToMenuList = (rawMenus: RawMenu[]): MenuListData => {
|
||||
const transformedMenus = rawMenus.map(transformRawMenu)
|
||||
|
||||
// 递归收集所有菜单项
|
||||
const collectAllMenus = (menu: Menu): Menu[] => {
|
||||
const result = [menu]
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.forEach(child => {
|
||||
result.push(...collectAllMenus(child))
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 收集所有菜单项
|
||||
const allMenus: Menu[] = []
|
||||
transformedMenus.forEach(menu => {
|
||||
allMenus.push(...collectAllMenus(menu))
|
||||
})
|
||||
|
||||
return {
|
||||
list: allMenus,
|
||||
total: allMenus.length,
|
||||
page: 1,
|
||||
page_size: allMenus.length
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单树结构
|
||||
const buildMenuTree = (rawMenus: RawMenu[]): Menu[] => {
|
||||
const transformedMenus = rawMenus.map(transformRawMenu)
|
||||
|
||||
// 创建菜单映射
|
||||
const menuMap = new Map<number, Menu>()
|
||||
transformedMenus.forEach(menu => {
|
||||
menuMap.set(menu.menu_id, { ...menu, children: [] })
|
||||
})
|
||||
|
||||
// 构建树结构
|
||||
const rootMenus: Menu[] = []
|
||||
transformedMenus.forEach(menu => {
|
||||
const menuItem = menuMap.get(menu.menu_id)!
|
||||
|
||||
if (menu.parent_id && menuMap.has(menu.parent_id)) {
|
||||
const parent = menuMap.get(menu.parent_id)!
|
||||
if (!parent.children) parent.children = []
|
||||
parent.children.push(menuItem)
|
||||
} else {
|
||||
rootMenus.push(menuItem)
|
||||
}
|
||||
})
|
||||
|
||||
return rootMenus
|
||||
}
|
||||
|
||||
// 菜单API
|
||||
export const menuApi = {
|
||||
// 获取菜单列表
|
||||
getMenuList: async (params?: MenuListParams): Promise<MenuListResponse> => {
|
||||
try {
|
||||
const response = await request.get<ApiResponse<RawMenu[]>>('/api/menus/', { params })
|
||||
|
||||
if (response.success && response.data && Array.isArray(response.data)) {
|
||||
const menuListData = transformToMenuList(response.data)
|
||||
return {
|
||||
success: true,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
data: menuListData
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: response.code || 500,
|
||||
message: response.message || '获取菜单数据失败',
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单列表失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '网络请求失败',
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取菜单树
|
||||
getMenuTree: async (): Promise<MenuTreeResponse> => {
|
||||
try {
|
||||
const response = await request.get<ApiResponse<RawMenu[]>>('/api/menus/tree/')
|
||||
|
||||
if (response.success && response.data && Array.isArray(response.data)) {
|
||||
// 调试:检查原始数据中的status值
|
||||
if (response.data.length > 0) {
|
||||
console.log('🔍 原始菜单数据status检查(前5条):', response.data.slice(0, 5).map((m: RawMenu) => ({
|
||||
menu_name: m.menu_name,
|
||||
menu_id: m.menu_id,
|
||||
status: m.status,
|
||||
statusType: typeof m.status
|
||||
})))
|
||||
}
|
||||
|
||||
// 后端已经返回树形结构,直接转换数据格式即可
|
||||
const transformedData = response.data.map(transformRawMenu)
|
||||
|
||||
// 调试:检查转换后的status值
|
||||
if (transformedData.length > 0) {
|
||||
console.log('🔍 转换后菜单数据status检查(前5条):', transformedData.slice(0, 5).map((m: Menu) => ({
|
||||
menu_name: m.menu_name,
|
||||
menu_id: m.menu_id,
|
||||
status: m.status,
|
||||
statusType: typeof m.status
|
||||
})))
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
data: transformedData
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: response.code || 500,
|
||||
message: response.message || '获取菜单树失败',
|
||||
data: []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单树失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '网络请求失败',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个菜单
|
||||
getMenu: async (id: number): Promise<ApiResponse<Menu>> => {
|
||||
try {
|
||||
const response = await request.get<ApiResponse<RawMenu>>(`/api/menus/${id}/`)
|
||||
|
||||
if (response.success && response.data) {
|
||||
const transformedMenu = transformRawMenu(response.data)
|
||||
return {
|
||||
success: true,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
data: transformedMenu
|
||||
}
|
||||
}
|
||||
|
||||
return response as ApiResponse<Menu>
|
||||
} catch (error) {
|
||||
console.error('获取菜单详情失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '网络请求失败',
|
||||
data: {} as Menu
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 创建菜单
|
||||
createMenu: (data: CreateMenuParams): Promise<ApiResponse<Menu>> => {
|
||||
return request.post('/api/menus/create/', data)
|
||||
},
|
||||
|
||||
// 更新菜单
|
||||
updateMenu: (id: number, data: UpdateMenuParams): Promise<ApiResponse<Menu>> => {
|
||||
return request.put(`/api/menus/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除菜单
|
||||
deleteMenu: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/menus/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除菜单
|
||||
batchDeleteMenus: (ids: number[]): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/menus/batch-delete/', { menu_ids: ids })
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 用户端通知模块 API 类型定义
|
||||
export interface UserNoticeListItem {
|
||||
notice: number
|
||||
title: string
|
||||
notice_type_display: string
|
||||
priority_display: string
|
||||
is_top: boolean
|
||||
publish_time: string
|
||||
is_read: boolean
|
||||
read_time: string | null
|
||||
is_starred: boolean
|
||||
starred_time: string | null
|
||||
is_expired: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface UserNoticeListData {
|
||||
notices: UserNoticeListItem[]
|
||||
pagination: {
|
||||
current_page: number
|
||||
page_size: number
|
||||
total_pages: number
|
||||
total_count: number
|
||||
has_next: boolean
|
||||
has_previous: boolean
|
||||
}
|
||||
statistics: {
|
||||
total_count: number
|
||||
unread_count: number
|
||||
starred_count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface UserNoticeDetailData {
|
||||
notice: number
|
||||
title: string
|
||||
content: string
|
||||
notice_type_display: string
|
||||
priority_display: string
|
||||
attachment_url: string | null
|
||||
publish_time: string
|
||||
expire_time: string
|
||||
is_top: boolean
|
||||
is_expired: boolean
|
||||
publisher_name: string | null
|
||||
is_read: boolean
|
||||
read_time: string
|
||||
is_starred: boolean
|
||||
starred_time: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export const noticeUserApi = {
|
||||
// 查看通知列表
|
||||
list: (params?: { page?: number; page_size?: number }): Promise<ApiResponse<UserNoticeListData>> =>
|
||||
request.get('/api/notice/user/list/', { params }),
|
||||
|
||||
// 查看通知详情
|
||||
detail: (notice_id: number | string): Promise<ApiResponse<UserNoticeDetailData>> =>
|
||||
request.get(`/api/notice/user/detail/${notice_id}/`),
|
||||
|
||||
// 标记通知已读
|
||||
markRead: (notice_id: number | string): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/notice/user/mark-read/', { notice_id }),
|
||||
|
||||
// 批量标记通知已读
|
||||
batchMarkRead: (notice_ids: Array<number | string>): Promise<ApiResponse<{ updated_count: number }>> =>
|
||||
request.post('/api/notice/user/batch-mark-read/', { notice_ids }),
|
||||
|
||||
// 用户获取通知统计
|
||||
statistics: (): Promise<ApiResponse<{ total_count: number; unread_count: number; read_count: number; starred_count: number; type_statistics?: Record<string, number>; priority_statistics?: Record<string, number> }>> =>
|
||||
request.get('/api/notice/user/statistics/'),
|
||||
|
||||
// 收藏/取消收藏通知
|
||||
toggleStar: (notice_id: number | string, is_starred: boolean): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/notice/user/toggle-star/', { notice_id, is_starred }),
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 修改密码接口参数
|
||||
export interface ChangePasswordParams {
|
||||
old_password: string
|
||||
new_password: string
|
||||
confirm_password: string
|
||||
}
|
||||
|
||||
// 重置密码接口参数
|
||||
export interface ResetPasswordParams {
|
||||
email: string
|
||||
email_code: string
|
||||
new_password: string
|
||||
confirm_password: string
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export const changePassword = (params: ChangePasswordParams) => {
|
||||
return request.post('/api/auth/password/change/', params)
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
export const resetPassword = (params: ResetPasswordParams) => {
|
||||
return request.post('/api/auth/password/reset/', params)
|
||||
}
|
||||
|
||||
// 发送重置密码邮箱验证码
|
||||
export const sendResetPasswordCode = (email: string) => {
|
||||
return request.post('/api/auth/password/reset/code/', { email })
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 权限接口类型定义
|
||||
export interface Permission {
|
||||
permission_id: number
|
||||
permission_name: string
|
||||
permission_code: string
|
||||
permission_type: 'menu' | 'button' | 'api'
|
||||
parent_id?: number
|
||||
path?: string
|
||||
icon?: string
|
||||
sort_order?: number
|
||||
description?: string
|
||||
status?: number
|
||||
children?: Permission[]
|
||||
}
|
||||
|
||||
// 角色接口类型定义
|
||||
export interface Role {
|
||||
role_id: number
|
||||
role_name: string
|
||||
role_code: string
|
||||
description?: string
|
||||
status?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
permissions?: Permission[]
|
||||
}
|
||||
|
||||
// API响应基础结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 角色列表数据结构
|
||||
export interface RoleListData {
|
||||
list: Role[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 角色列表响应类型
|
||||
export type RoleListResponse = ApiResponse<RoleListData>
|
||||
|
||||
// 角色列表查询参数
|
||||
export interface RoleListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
// 创建角色参数
|
||||
export interface CreateRoleParams {
|
||||
role_name: string
|
||||
role_code: string
|
||||
description?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
// 更新角色参数
|
||||
export type UpdateRoleParams = Partial<CreateRoleParams>
|
||||
|
||||
// 角色权限分配参数
|
||||
export interface AssignRolePermissionsParams {
|
||||
role_id: number
|
||||
menu_ids: number[]
|
||||
user_type?: number
|
||||
department_id?: number
|
||||
}
|
||||
|
||||
// 权限列表响应类型
|
||||
export type PermissionListResponse = ApiResponse<Permission[]>
|
||||
|
||||
// 角色API
|
||||
export const roleApi = {
|
||||
// 获取角色列表
|
||||
getRoleList: (params?: RoleListParams): Promise<RoleListResponse> => {
|
||||
return request.get('/api/roles/', { params })
|
||||
},
|
||||
|
||||
// 获取单个角色
|
||||
getRole: (id: number): Promise<ApiResponse<Role>> => {
|
||||
return request.get(`/api/roles/${id}/`)
|
||||
},
|
||||
|
||||
// 创建角色
|
||||
createRole: (data: CreateRoleParams): Promise<ApiResponse<Role>> => {
|
||||
return request.post('/api/roles/create/', data)
|
||||
},
|
||||
|
||||
// 更新角色
|
||||
updateRole: (id: number, data: UpdateRoleParams): Promise<ApiResponse<Role>> => {
|
||||
return request.put(`/api/roles/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除角色
|
||||
deleteRole: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/roles/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除角色
|
||||
batchDeleteRoles: (ids: number[]): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/roles/batch-delete/', { role_ids: ids })
|
||||
},
|
||||
|
||||
// 获取角色权限
|
||||
getRolePermissions: (id: number): Promise<ApiResponse<Permission[]>> => {
|
||||
return request.get(`/api/roles/${id}/menus/`)
|
||||
},
|
||||
|
||||
// 分配角色权限
|
||||
assignRolePermissions: (data: AssignRolePermissionsParams): Promise<ApiResponse<any>> => {
|
||||
return request.post(`/api/roles/assign-menus/`, data)
|
||||
},
|
||||
|
||||
// 获取所有权限列表
|
||||
getPermissionList: (): Promise<PermissionListResponse> => {
|
||||
return request.get('/api/menus/')
|
||||
},
|
||||
|
||||
// 获取权限树
|
||||
getPermissionTree: (): Promise<PermissionListResponse> => {
|
||||
return request.get('/api/menus/tree/')
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 1. 系统信息
|
||||
export interface SystemInfo {
|
||||
hostname: string
|
||||
platform: string
|
||||
architecture: string
|
||||
boot_time: string
|
||||
uptime: string
|
||||
}
|
||||
|
||||
// 2. CPU 信息
|
||||
export interface CpuInfo {
|
||||
cpu_count: number
|
||||
cpu_percent: number
|
||||
cpu_freq: {
|
||||
current: number
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
load_avg: number[]
|
||||
}
|
||||
|
||||
// 3. 内存信息
|
||||
export interface MemoryInfo {
|
||||
total: number
|
||||
available: number
|
||||
used: number
|
||||
percent: number
|
||||
free: number
|
||||
}
|
||||
|
||||
// 4. 磁盘信息
|
||||
export interface DiskInfo {
|
||||
device: string
|
||||
mountpoint: string
|
||||
fstype: string
|
||||
total: number
|
||||
used: number
|
||||
free: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
// 5. 网络信息
|
||||
export interface NetworkInfo {
|
||||
interface: string
|
||||
bytes_sent: number
|
||||
bytes_recv: number
|
||||
packets_sent: number
|
||||
packets_recv: number
|
||||
}
|
||||
|
||||
// 6. 进程信息
|
||||
export interface ProcessInfo {
|
||||
pid: number
|
||||
name: string
|
||||
status: string
|
||||
cpu_percent: number
|
||||
memory_percent: number
|
||||
memory_info: {
|
||||
rss: number
|
||||
vms: number
|
||||
}
|
||||
create_time: string
|
||||
cmdline: string[]
|
||||
}
|
||||
|
||||
// 7. GPU 信息
|
||||
export interface GpuInfoItem {
|
||||
id: number
|
||||
name: string
|
||||
load: number
|
||||
memory_total: number
|
||||
memory_used: number
|
||||
memory_util: number
|
||||
temperature: number
|
||||
}
|
||||
|
||||
export interface GpuInfoResponse {
|
||||
gpu_available: boolean
|
||||
gpu_info?: GpuInfoItem[]
|
||||
message?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
// 8. 综合监测信息
|
||||
export interface MonitorData {
|
||||
system: SystemInfo
|
||||
cpu: CpuInfo
|
||||
memory: MemoryInfo
|
||||
disks: DiskInfo[]
|
||||
network: NetworkInfo[]
|
||||
processes: ProcessInfo[]
|
||||
gpus: Array<{ gpu_available: boolean; message?: string; timestamp: string }>
|
||||
}
|
||||
|
||||
export const systemMonitorApi = {
|
||||
getSystem: (): Promise<ApiResponse<SystemInfo>> => request.get('/api/system/system/'),
|
||||
getCpu: (): Promise<ApiResponse<CpuInfo>> => request.get('/api/system/cpu/'),
|
||||
getMemory: (): Promise<ApiResponse<MemoryInfo>> => request.get('/api/system/memory/'),
|
||||
getDisks: (): Promise<ApiResponse<DiskInfo[]>> => request.get('/api/system/disks/'),
|
||||
getNetwork: (): Promise<ApiResponse<NetworkInfo[]>> => request.get('/api/system/network/'),
|
||||
getProcesses: (): Promise<ApiResponse<ProcessInfo[]>> => request.get('/api/system/processes/'),
|
||||
getGpu: (): Promise<ApiResponse<GpuInfoResponse>> => request.get('/api/system/gpu/'),
|
||||
getMonitor: (): Promise<ApiResponse<MonitorData>> => request.get('/api/system/monitor/'),
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 角色接口类型定义
|
||||
export interface Role {
|
||||
role_id: number
|
||||
role_name: string
|
||||
role_code: string
|
||||
role_ids?: string
|
||||
}
|
||||
|
||||
// 用户接口类型定义(匹配后端实际数据结构)
|
||||
export interface User {
|
||||
user_id: number
|
||||
username: string
|
||||
email: string
|
||||
phone?: string
|
||||
real_name?: string
|
||||
avatar?: string
|
||||
gender: number
|
||||
birthday?: string
|
||||
department_id?: number
|
||||
status: number
|
||||
last_login_time?: string
|
||||
last_login_ip?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
roles: Role[]
|
||||
}
|
||||
|
||||
// API响应基础结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 用户列表数据结构
|
||||
export interface UserListData {
|
||||
list: User[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 用户列表响应类型
|
||||
export type UserListResponse = ApiResponse<UserListData>
|
||||
|
||||
// 用户列表查询参数
|
||||
export interface UserListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
role_ids?: string
|
||||
}
|
||||
|
||||
// 分配角色参数
|
||||
export interface AssignRolesParams {
|
||||
user_id: number
|
||||
role_ids: number[] // 角色ID数组
|
||||
}
|
||||
|
||||
// 用户API
|
||||
export const userApi = {
|
||||
// 获取用户列表
|
||||
getUserList: (params?: UserListParams): Promise<UserListResponse> => {
|
||||
return request.get('/api/users/', { params })
|
||||
},
|
||||
|
||||
// 获取单个用户
|
||||
getUser: (id: number): Promise<ApiResponse<User>> => {
|
||||
return request.get(`/api/users/${id}/`)
|
||||
},
|
||||
|
||||
// 创建用户
|
||||
createUser: (data: Partial<User>): Promise<ApiResponse<User>> => {
|
||||
return request.post('/api/users/create/', data)
|
||||
},
|
||||
|
||||
// 更新用户
|
||||
updateUser: (id: number, data: Partial<User>): Promise<ApiResponse<User>> => {
|
||||
return request.put(`/api/users/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
deleteUser: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/users/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除用户
|
||||
batchDeleteUsers: (ids: number[]): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/admin/users/batch-delete/', { user_ids: ids })
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
getUserInfo: (): Promise<ApiResponse<User>> => {
|
||||
return request.get('/api/auth/user/info/')
|
||||
},
|
||||
|
||||
// 更新当前用户信息
|
||||
updateUserInfo: (data: Partial<User>): Promise<ApiResponse<User>> => {
|
||||
return request.put('/api/auth/user/info/update/', data)
|
||||
},
|
||||
|
||||
// 分配用户角色
|
||||
assignRoles: (data: AssignRolesParams): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/users/assign-roles/', data)
|
||||
},
|
||||
|
||||
// 获取所有角色列表
|
||||
getRoleList: (): Promise<ApiResponse<Role[]>> => {
|
||||
return request.get('/api/roles/')
|
||||
}
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// YOLO检测相关接口类型定义
|
||||
export interface YoloDetectionRequest {
|
||||
image: File
|
||||
model_id?: string
|
||||
confidence_threshold?: number
|
||||
nms_threshold?: number
|
||||
}
|
||||
|
||||
export interface DetectionBbox {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface YoloDetection {
|
||||
class_id: number
|
||||
class_name: string
|
||||
confidence: number
|
||||
bbox: DetectionBbox
|
||||
}
|
||||
|
||||
export interface YoloDetectionResponse {
|
||||
message: string
|
||||
data?: {
|
||||
detection_id: number
|
||||
result_file_url: string
|
||||
original_file_url: string
|
||||
object_count: number
|
||||
detected_categories: string[]
|
||||
confidence_scores: number[]
|
||||
avg_confidence: number | null
|
||||
processing_time: number
|
||||
model_used: string
|
||||
confidence_threshold: number
|
||||
user_id: number
|
||||
user_name: string
|
||||
alert_level?: 'low' | 'medium' | 'high'
|
||||
}
|
||||
}
|
||||
|
||||
export interface YoloModel {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
description: string
|
||||
classes: string[]
|
||||
is_active: boolean
|
||||
is_enabled: boolean
|
||||
model_file: string
|
||||
model_folder_path: string
|
||||
model_path: string
|
||||
weights_folder_path: string
|
||||
categories: { [key: string]: any }
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface YoloModelListResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: {
|
||||
models: YoloModel[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
// YOLO检测API
|
||||
export const yoloApi = {
|
||||
// 执行YOLO检测
|
||||
async detectImage(detectionRequest: YoloDetectionRequest): Promise<YoloDetectionResponse> {
|
||||
console.log('🔍 构建检测请求:', detectionRequest)
|
||||
console.log('📁 文件对象详情:', {
|
||||
name: detectionRequest.image.name,
|
||||
size: detectionRequest.image.size,
|
||||
type: detectionRequest.image.type,
|
||||
lastModified: detectionRequest.image.lastModified
|
||||
})
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', detectionRequest.image)
|
||||
|
||||
if (detectionRequest.model_id) {
|
||||
formData.append('model_id', detectionRequest.model_id)
|
||||
}
|
||||
if (detectionRequest.confidence_threshold) {
|
||||
formData.append('confidence_threshold', detectionRequest.confidence_threshold.toString())
|
||||
}
|
||||
if (detectionRequest.nms_threshold) {
|
||||
formData.append('nms_threshold', detectionRequest.nms_threshold.toString())
|
||||
}
|
||||
|
||||
// 调试FormData内容
|
||||
console.log('📤 FormData内容:')
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
console.log(` ${key}: File(${value.name}, ${value.size} bytes, ${value.type})`)
|
||||
} else {
|
||||
console.log(` ${key}:`, value)
|
||||
}
|
||||
}
|
||||
|
||||
return request.post('/api/yolo/detect/', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取当前启用的YOLO模型信息
|
||||
async getCurrentEnabledModel(): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.get('/api/yolo/models/enabled/')
|
||||
},
|
||||
|
||||
// 获取模型详情
|
||||
async getModelInfo(modelId: string): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.get(`/api/yolo/models/${modelId}`)
|
||||
},
|
||||
|
||||
// 批量检测
|
||||
async detectBatch(images: File[], modelId?: string): Promise<YoloDetectionResponse[]> {
|
||||
const promises = images.map(image =>
|
||||
this.detectImage({
|
||||
image,
|
||||
model_id: modelId,
|
||||
confidence_threshold: 0.5,
|
||||
nms_threshold: 0.4
|
||||
})
|
||||
)
|
||||
|
||||
return Promise.all(promises)
|
||||
},
|
||||
|
||||
// 获取模型列表
|
||||
async getModels(): Promise<{ success: boolean; data?: YoloModel[]; message?: string }> {
|
||||
return request.get('/api/yolo/models/')
|
||||
},
|
||||
|
||||
// 上传模型
|
||||
async uploadModel(formData: FormData): Promise<{ success: boolean; message?: string }> {
|
||||
// 使用专门的upload方法,它会自动处理Content-Type
|
||||
return request.upload('/api/yolo/upload/', formData)
|
||||
},
|
||||
|
||||
// 更新模型信息
|
||||
async updateModel(modelId: string, data: { name: string; version: string }): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.put(`/api/yolo/models/${modelId}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除模型
|
||||
async deleteModel(modelId: string): Promise<{ success: boolean; message?: string }> {
|
||||
return request.delete(`/api/yolo/models/${modelId}/delete/`)
|
||||
},
|
||||
|
||||
// 启用模型
|
||||
async enableModel(modelId: string): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.post(`/api/yolo/models/${modelId}/enable/`)
|
||||
},
|
||||
|
||||
// 获取模型详情
|
||||
async getModelDetail(modelId: string): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.get(`/api/yolo/models/${modelId}/`)
|
||||
},
|
||||
|
||||
// 获取检测历史记录列表
|
||||
async getDetectionHistory(params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
model_id?: string
|
||||
}): Promise<{ success: boolean; data?: DetectionHistoryRecord[]; message?: string }> {
|
||||
return request.get('/api/yolo/detections/', { params })
|
||||
},
|
||||
|
||||
// 获取检测记录详情
|
||||
async getDetectionDetail(recordId: string): Promise<{ success: boolean; data?: DetectionHistoryRecord; message?: string }> {
|
||||
return request.get(`/api/detections/${recordId}/`)
|
||||
},
|
||||
|
||||
// 删除检测记录
|
||||
async deleteDetection(recordId: string): Promise<{ success: boolean; message?: string }> {
|
||||
return request.delete(`/api/yolo/detections/${recordId}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除检测记录
|
||||
async batchDeleteDetections(ids: number[]): Promise<{ success: boolean; message?: string }> {
|
||||
return request.post('/api/yolo/detections/batch-delete/', { ids })
|
||||
},
|
||||
|
||||
// 获取检测统计
|
||||
async getDetectionStats(): Promise<{ success: boolean; data?: any; message?: string }> {
|
||||
return request.get('/api/yolo/stats/')
|
||||
},
|
||||
|
||||
// 警告等级管理相关接口
|
||||
// 获取警告等级列表
|
||||
async getAlertLevels(): Promise<{ success: boolean; data?: AlertLevel[]; message?: string }> {
|
||||
return request.get('/api/yolo/categories/')
|
||||
},
|
||||
|
||||
// 获取警告等级详情
|
||||
async getAlertLevelDetail(levelId: string): Promise<{ success: boolean; data?: AlertLevel; message?: string }> {
|
||||
return request.get(`/api/yolo/categories/${levelId}/`)
|
||||
},
|
||||
|
||||
// 更新警告等级
|
||||
async updateAlertLevel(levelId: string, data: { alert_level?: 'low' | 'medium' | 'high'; alias?: string }): Promise<{ success: boolean; data?: AlertLevel; message?: string }> {
|
||||
return request.put(`/api/yolo/categories/${levelId}/update/`, data)
|
||||
},
|
||||
|
||||
// 切换警告等级状态
|
||||
async toggleAlertLevelStatus(levelId: string): Promise<{ success: boolean; data?: AlertLevel; message?: string }> {
|
||||
return request.post(`/api/yolo/categories/${levelId}/toggle-status/`)
|
||||
},
|
||||
|
||||
// 获取活跃的警告等级列表
|
||||
async getActiveAlertLevels(): Promise<{ success: boolean; data?: AlertLevel[]; message?: string }> {
|
||||
return request.get('/api/yolo/categories/active/')
|
||||
},
|
||||
|
||||
// 上传并转换PT模型为ONNX格式
|
||||
async uploadAndConvertToOnnx(formData: FormData): Promise<{
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: {
|
||||
onnx_path?: string
|
||||
onnx_url?: string
|
||||
download_url?: string
|
||||
onnx_relative_path?: string
|
||||
file_name?: string
|
||||
labels_download_url?: string
|
||||
labels_relative_path?: string
|
||||
classes?: string[]
|
||||
}
|
||||
}> {
|
||||
// 适配后端 @views.py 中的 upload_pt_convert_onnx 实现
|
||||
// 统一走 /api/upload_pt_convert_onnx
|
||||
// 按你的后端接口:/yolo/onnx/upload/
|
||||
// 注意带上结尾斜杠,避免 404
|
||||
return request.upload('/api/yolo/onnx/upload/', formData)
|
||||
}
|
||||
}
|
||||
|
||||
// 警告等级管理相关接口
|
||||
export interface AlertLevel {
|
||||
id: number
|
||||
model: number
|
||||
model_name: string
|
||||
name: string
|
||||
alias: string
|
||||
display_name: string
|
||||
category_id: number
|
||||
alert_level: 'low' | 'medium' | 'high'
|
||||
alert_level_display: string
|
||||
is_active: boolean
|
||||
// 前端编辑状态字段
|
||||
editingAlias?: boolean
|
||||
tempAlias?: string
|
||||
}
|
||||
|
||||
// 用户检测历史相关接口
|
||||
export interface DetectionHistoryRecord {
|
||||
id: number
|
||||
user_id: number
|
||||
original_filename: string
|
||||
result_filename: string
|
||||
original_file: string
|
||||
result_file: string
|
||||
detection_type: 'image' | 'video'
|
||||
object_count: number
|
||||
detected_categories: string[]
|
||||
confidence_scores: number[]
|
||||
avg_confidence: number | null
|
||||
processing_time: number
|
||||
model_name: string
|
||||
model_info: any
|
||||
created_at: string
|
||||
confidence_threshold?: number // 置信度阈值(原始设置值)
|
||||
// 为了兼容前端显示,添加计算字段
|
||||
filename?: string
|
||||
image_url?: string
|
||||
detections?: YoloDetection[]
|
||||
}
|
||||
|
||||
export interface DetectionHistoryParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
class_filter?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
model_id?: string
|
||||
}
|
||||
|
||||
export interface DetectionHistoryResponse {
|
||||
success?: boolean
|
||||
message?: string
|
||||
data?: {
|
||||
records: DetectionHistoryRecord[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
} | DetectionHistoryRecord[]
|
||||
// 支持直接返回数组的情况
|
||||
results?: DetectionHistoryRecord[]
|
||||
count?: number
|
||||
// 支持Django REST framework的分页格式
|
||||
next?: string
|
||||
previous?: string
|
||||
}
|
||||
|
||||
// 用户检测历史API
|
||||
export const detectionHistoryApi = {
|
||||
// 获取用户检测历史
|
||||
async getUserDetectionHistory(userId: number, params: DetectionHistoryParams = {}): Promise<DetectionHistoryResponse> {
|
||||
return request.get('/api/yolo/detections/', {
|
||||
params: {
|
||||
user_id: userId,
|
||||
...params
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取检测记录详情
|
||||
async getDetectionRecordDetail(recordId: number): Promise<{
|
||||
success?: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: DetectionHistoryRecord
|
||||
}> {
|
||||
return request.get(`/api/yolo/detections/${recordId}/`)
|
||||
},
|
||||
|
||||
// 删除检测记录
|
||||
async deleteDetectionRecord(userId: number, recordId: string): Promise<{ success: boolean; message?: string }> {
|
||||
return request.delete(`/api/yolo/detections/${recordId}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除检测记录
|
||||
async batchDeleteDetectionRecords(userId: number, recordIds: string[]): Promise<{ success: boolean; message?: string }> {
|
||||
return request.post('/api/yolo/detections/batch-delete/', { ids: recordIds })
|
||||
},
|
||||
|
||||
// 导出检测历史
|
||||
async exportDetectionHistory(userId: number, params: DetectionHistoryParams = {}): Promise<Blob> {
|
||||
const response = await request.get('/api/yolo/detections/export/', {
|
||||
params: {
|
||||
user_id: userId,
|
||||
...params
|
||||
},
|
||||
responseType: 'blob'
|
||||
})
|
||||
return response
|
||||
},
|
||||
|
||||
// 获取检测统计信息
|
||||
async getDetectionStats(userId: number): Promise<{
|
||||
success: boolean
|
||||
data?: {
|
||||
total_detections: number
|
||||
total_images: number
|
||||
class_counts: Record<string, number>
|
||||
recent_activity: Array<{
|
||||
date: string
|
||||
count: number
|
||||
}>
|
||||
}
|
||||
message?: string
|
||||
}> {
|
||||
return request.get('/api/yolo/detections/stats/', {
|
||||
params: { user_id: userId }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 告警相关接口类型定义
|
||||
export interface AlertRecord {
|
||||
id: number
|
||||
detection_record: number
|
||||
detection_info: {
|
||||
id: number
|
||||
detection_type: string
|
||||
original_filename: string
|
||||
result_filename: string
|
||||
object_count: number
|
||||
avg_confidence: number
|
||||
}
|
||||
user: number
|
||||
user_name: string
|
||||
alert_level: string
|
||||
alert_level_display: string
|
||||
alert_category: string
|
||||
category: number
|
||||
category_info: {
|
||||
id: number
|
||||
name: string
|
||||
alert_level: string
|
||||
alert_level_display: string
|
||||
}
|
||||
status: string
|
||||
created_at: string
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
// 告警管理API
|
||||
export const alertApi = {
|
||||
// 获取所有告警记录
|
||||
async getAllAlerts(): Promise<{ success: boolean; data?: AlertRecord[]; message?: string }> {
|
||||
return request.get('/api/yolo/alerts/')
|
||||
},
|
||||
|
||||
// 获取当前用户的告警记录
|
||||
async getUserAlerts(userId: string): Promise<{ success: boolean; data?: AlertRecord[]; message?: string }> {
|
||||
return request.get(`/api/yolo/users/${userId}/alerts/`)
|
||||
},
|
||||
|
||||
// 处理告警(更新状态)
|
||||
async updateAlertStatus(alertId: string, status: string): Promise<{ success: boolean; data?: AlertRecord; message?: string }> {
|
||||
return request.put(`/api/yolo/alerts/${alertId}/update-status/`, { status })
|
||||
}
|
||||
}
|
||||
|
||||
// 默认导出
|
||||
export default yoloApi
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ msg: string }>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
@@ -1,159 +0,0 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
add: 'Add',
|
||||
search: 'Search',
|
||||
reset: 'Reset',
|
||||
loading: 'Loading...',
|
||||
noData: 'No Data',
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
warning: 'Warning',
|
||||
info: 'Info',
|
||||
},
|
||||
nav: {
|
||||
home: 'Home',
|
||||
dashboard: 'Dashboard',
|
||||
user: 'User Management',
|
||||
role: 'Role Management',
|
||||
menu: 'Menu Management',
|
||||
settings: 'System Settings',
|
||||
profile: 'Profile',
|
||||
logout: 'Logout',
|
||||
},
|
||||
login: {
|
||||
title: 'Login',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
login: 'Login',
|
||||
forgotPassword: 'Forgot Password?',
|
||||
rememberMe: 'Remember Me',
|
||||
},
|
||||
success: {
|
||||
// General success messages
|
||||
operationSuccess: 'Operation Successful',
|
||||
saveSuccess: 'Save Successful',
|
||||
deleteSuccess: 'Delete Successful',
|
||||
updateSuccess: 'Update Successful',
|
||||
|
||||
// Login and registration related success messages
|
||||
loginSuccess: 'Login Successful',
|
||||
registerSuccess: 'Registration Successful! Please Login',
|
||||
logoutSuccess: 'Logout Successful',
|
||||
emailCodeSent: 'Verification Code Sent to Your Email',
|
||||
|
||||
// User management related success messages
|
||||
userCreated: 'User Created Successfully',
|
||||
userUpdated: 'User Information Updated Successfully',
|
||||
userDeleted: 'User Deleted Successfully',
|
||||
roleAssigned: 'Role Assigned Successfully',
|
||||
|
||||
// Other operation success messages
|
||||
uploadSuccess: 'File Upload Successful',
|
||||
downloadSuccess: 'File Download Successful',
|
||||
copySuccess: 'Copy Successful',
|
||||
},
|
||||
error: {
|
||||
// General errors
|
||||
// 404: 'Page Not Found',
|
||||
403: 'Access Denied, Please Contact Administrator',
|
||||
500: 'Internal Server Error, Please Try Again Later',
|
||||
networkError: 'Network Connection Failed, Please Check Network Settings',
|
||||
timeout: 'Request Timeout, Please Try Again Later',
|
||||
|
||||
// Login related errors
|
||||
loginFailed: 'Login Failed, Please Check Username and Password',
|
||||
usernameRequired: 'Please Enter Username',
|
||||
passwordRequired: 'Please Enter Password',
|
||||
captchaRequired: 'Please Enter Captcha',
|
||||
captchaError: 'Captcha Error, Please Re-enter (Case Sensitive)',
|
||||
captchaExpired: 'Captcha Expired, Please Refresh and Re-enter',
|
||||
accountLocked: 'Account Locked, Please Contact Administrator',
|
||||
accountDisabled: 'Account Disabled, Please Contact Administrator',
|
||||
passwordExpired: 'Password Expired, Please Change Password',
|
||||
loginAttemptsExceeded: 'Too Many Login Attempts, Account Temporarily Locked',
|
||||
|
||||
// Registration related errors
|
||||
registerFailed: 'Registration Failed, Please Check Input Information',
|
||||
usernameExists: 'Username Already Exists, Please Choose Another',
|
||||
emailExists: 'Email Already Registered, Please Use Another Email',
|
||||
phoneExists: 'Phone Number Already Registered, Please Use Another',
|
||||
emailFormatError: 'Invalid Email Format, Please Enter Valid Email',
|
||||
phoneFormatError: 'Invalid Phone Format, Please Enter 11-digit Phone Number',
|
||||
passwordTooWeak: 'Password Too Weak, Please Include Uppercase, Lowercase, Numbers and Special Characters',
|
||||
passwordMismatch: 'Passwords Do Not Match',
|
||||
emailCodeError: 'Email Verification Code Error or Expired',
|
||||
emailCodeRequired: 'Please Enter Email Verification Code',
|
||||
emailCodeLength: 'Verification Code Must Be 6 Digits',
|
||||
emailRequired: 'Please Enter Email',
|
||||
usernameLength: 'Username Length Must Be 3-20 Characters',
|
||||
passwordLength: 'Password Length Must Be 6-20 Characters',
|
||||
confirmPasswordRequired: 'Please Confirm Password',
|
||||
phoneRequired: 'Please Enter Phone Number',
|
||||
realNameRequired: 'Please Enter Real Name',
|
||||
realNameLength: 'Name Length Must Be 2-10 Characters',
|
||||
|
||||
// Permission related errors
|
||||
accessDenied: 'Access Denied, You Do Not Have Permission to Perform This Action',
|
||||
roleNotFound: 'Role Not Found or Deleted',
|
||||
permissionDenied: 'Permission Denied, Cannot Perform This Action',
|
||||
tokenExpired: 'Login Expired, Please Login Again',
|
||||
tokenInvalid: 'Invalid Login Status, Please Login Again',
|
||||
|
||||
// User management related errors
|
||||
userNotFound: 'User Not Found or Deleted',
|
||||
userCreateFailed: 'Failed to Create User, Please Check Input Information',
|
||||
userUpdateFailed: 'Failed to Update User Information',
|
||||
userDeleteFailed: 'Failed to Delete User, User May Be In Use',
|
||||
cannotDeleteSelf: 'Cannot Delete Your Own Account',
|
||||
cannotDeleteAdmin: 'Cannot Delete Administrator Account',
|
||||
|
||||
// Department management related errors
|
||||
departmentNotFound: 'Department Not Found or Deleted',
|
||||
departmentNameExists: 'Department Name Already Exists',
|
||||
departmentHasUsers: 'Department Has Users, Cannot Delete',
|
||||
departmentCreateFailed: 'Failed to Create Department',
|
||||
departmentUpdateFailed: 'Failed to Update Department Information',
|
||||
departmentDeleteFailed: 'Failed to Delete Department',
|
||||
|
||||
// Role management related errors
|
||||
roleNameExists: 'Role Name Already Exists',
|
||||
roleCreateFailed: 'Failed to Create Role',
|
||||
roleUpdateFailed: 'Failed to Update Role Information',
|
||||
roleDeleteFailed: 'Failed to Delete Role',
|
||||
roleInUse: 'Role In Use, Cannot Delete',
|
||||
|
||||
// File upload related errors
|
||||
fileUploadFailed: 'File Upload Failed',
|
||||
fileSizeExceeded: 'File Size Exceeded Limit',
|
||||
fileTypeNotSupported: 'File Type Not Supported',
|
||||
fileRequired: 'Please Select File to Upload',
|
||||
|
||||
// Data validation related errors
|
||||
invalidInput: 'Invalid Input Data Format',
|
||||
requiredFieldMissing: 'Required Field Cannot Be Empty',
|
||||
fieldTooLong: 'Input Content Exceeds Length Limit',
|
||||
fieldTooShort: 'Input Content Length Insufficient',
|
||||
invalidDate: 'Invalid Date Format',
|
||||
invalidNumber: 'Invalid Number Format',
|
||||
|
||||
// Operation related errors
|
||||
operationFailed: 'Operation Failed, Please Try Again Later',
|
||||
saveSuccess: 'Save Successful',
|
||||
saveFailed: 'Save Failed, Please Check Input Information',
|
||||
deleteSuccess: 'Delete Successful',
|
||||
deleteFailed: 'Delete Failed, Please Try Again Later',
|
||||
updateSuccess: 'Update Successful',
|
||||
updateFailed: 'Update Failed, Please Check Input Information',
|
||||
|
||||
// System related errors
|
||||
systemMaintenance: 'System Under Maintenance, Please Visit Later',
|
||||
serviceUnavailable: 'Service Temporarily Unavailable, Please Try Again Later',
|
||||
databaseError: 'Database Connection Error, Please Contact Technical Support',
|
||||
configError: 'System Configuration Error, Please Contact Administrator',
|
||||
},
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zhCN from './zh-CN'
|
||||
import enUS from './en-US'
|
||||
|
||||
const messages = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: 'zh-CN',
|
||||
fallbackLocale: 'en-US',
|
||||
messages,
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,172 +0,0 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
add: '添 加',
|
||||
search: '搜索',
|
||||
reset: '重置',
|
||||
loading: '加载中...',
|
||||
noData: '暂无数据',
|
||||
success: '成功',
|
||||
error: '错误',
|
||||
warning: '警告',
|
||||
info: '提示',
|
||||
},
|
||||
nav: {
|
||||
home: '首页',
|
||||
dashboard: '仪表板',
|
||||
user: '用户管理',
|
||||
role: '角色管理',
|
||||
menu: '菜单管理',
|
||||
settings: '系统设置',
|
||||
profile: '个人资料',
|
||||
logout: '退出登录',
|
||||
},
|
||||
login: {
|
||||
title: '登录',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
login: '登录',
|
||||
forgotPassword: '忘记密码?',
|
||||
rememberMe: '记住我',
|
||||
},
|
||||
register: {
|
||||
title: '注册',
|
||||
username: '用户名',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
confirmPassword: '确认密码',
|
||||
register: '注册',
|
||||
agreement: '我已阅读并同意',
|
||||
userAgreement: '用户协议',
|
||||
privacyPolicy: '隐私政策',
|
||||
hasAccount: '已有账号?',
|
||||
goToLogin: '立即登录',
|
||||
},
|
||||
success: {
|
||||
// 通用成功提示
|
||||
operationSuccess: '操作成功',
|
||||
saveSuccess: '保存成功',
|
||||
deleteSuccess: '删除成功',
|
||||
updateSuccess: '更新成功',
|
||||
|
||||
// 登录注册相关成功提示
|
||||
loginSuccess: '登录成功',
|
||||
registerSuccess: '注册成功!请前往登录',
|
||||
logoutSuccess: '退出登录成功',
|
||||
emailCodeSent: '验证码已发送到您的邮箱',
|
||||
|
||||
// 用户管理相关成功提示
|
||||
userCreated: '用户创建成功',
|
||||
userUpdated: '用户信息更新成功',
|
||||
userDeleted: '用户删除成功',
|
||||
roleAssigned: '角色分配成功',
|
||||
|
||||
// 其他操作成功提示
|
||||
uploadSuccess: '文件上传成功',
|
||||
downloadSuccess: '文件下载成功',
|
||||
copySuccess: '复制成功',
|
||||
},
|
||||
error: {
|
||||
// 通用错误
|
||||
// 404: '页面未找到',
|
||||
403: '权限不足,请联系管理员',
|
||||
500: '服务器内部错误,请稍后重试',
|
||||
networkError: '网络连接失败,请检查网络设置',
|
||||
timeout: '请求超时,请稍后重试',
|
||||
|
||||
// 登录相关错误
|
||||
loginFailed: '登录失败,请检查用户名和密码',
|
||||
usernameRequired: '请输入用户名',
|
||||
passwordRequired: '请输入密码',
|
||||
captchaRequired: '请输入验证码',
|
||||
captchaError: '验证码错误,请重新输入(区分大小写)',
|
||||
captchaExpired: '验证码已过期,请刷新后重新输入',
|
||||
accountLocked: '账户已被锁定,请联系管理员',
|
||||
accountDisabled: '账户已被禁用,请联系管理员',
|
||||
passwordExpired: '密码已过期,请修改密码',
|
||||
loginAttemptsExceeded: '登录尝试次数过多,账户已被临时锁定',
|
||||
|
||||
// 注册相关错误
|
||||
registerFailed: '注册失败,请检查输入信息',
|
||||
usernameExists: '用户名已存在,请选择其他用户名',
|
||||
emailExists: '邮箱已被注册,请使用其他邮箱',
|
||||
phoneExists: '手机号已被注册,请使用其他手机号',
|
||||
emailFormatError: '邮箱格式不正确,请输入有效的邮箱地址',
|
||||
phoneFormatError: '手机号格式不正确,请输入11位手机号',
|
||||
passwordTooWeak: '密码强度不足,请包含大小写字母、数字和特殊字符',
|
||||
passwordMismatch: '两次输入的密码不一致',
|
||||
emailCodeError: '邮箱验证码错误或已过期',
|
||||
emailCodeRequired: '请输入邮箱验证码',
|
||||
emailCodeLength: '验证码长度为6位',
|
||||
emailRequired: '请输入邮箱',
|
||||
usernameLength: '用户名长度为3-20个字符',
|
||||
passwordLength: '密码长度为6-20个字符',
|
||||
confirmPasswordRequired: '请确认密码',
|
||||
phoneRequired: '请输入手机号',
|
||||
realNameRequired: '请输入真实姓名',
|
||||
realNameLength: '姓名长度为2-10个字符',
|
||||
|
||||
// 权限相关错误
|
||||
accessDenied: '访问被拒绝,您没有执行此操作的权限',
|
||||
roleNotFound: '角色不存在或已被删除',
|
||||
permissionDenied: '权限不足,无法执行此操作',
|
||||
tokenExpired: '登录已过期,请重新登录',
|
||||
tokenInvalid: '登录状态无效,请重新登录',
|
||||
|
||||
// 用户管理相关错误
|
||||
userNotFound: '用户不存在或已被删除',
|
||||
userCreateFailed: '创建用户失败,请检查输入信息',
|
||||
userUpdateFailed: '更新用户信息失败',
|
||||
userDeleteFailed: '删除用户失败,该用户可能正在使用中',
|
||||
cannotDeleteSelf: '不能删除自己的账户',
|
||||
cannotDeleteAdmin: '不能删除管理员账户',
|
||||
|
||||
// 部门管理相关错误
|
||||
departmentNotFound: '部门不存在或已被删除',
|
||||
departmentNameExists: '部门名称已存在',
|
||||
departmentHasUsers: '部门下还有用户,无法删除',
|
||||
departmentCreateFailed: '创建部门失败',
|
||||
departmentUpdateFailed: '更新部门信息失败',
|
||||
departmentDeleteFailed: '删除部门失败',
|
||||
|
||||
// 角色管理相关错误
|
||||
roleNameExists: '角色名称已存在',
|
||||
roleCreateFailed: '创建角色失败',
|
||||
roleUpdateFailed: '更新角色信息失败',
|
||||
roleDeleteFailed: '删除角色失败',
|
||||
roleInUse: '角色正在使用中,无法删除',
|
||||
|
||||
// 文件上传相关错误
|
||||
fileUploadFailed: '文件上传失败',
|
||||
fileSizeExceeded: '文件大小超出限制',
|
||||
fileTypeNotSupported: '不支持的文件类型',
|
||||
fileRequired: '请选择要上传的文件',
|
||||
|
||||
// 数据验证相关错误
|
||||
invalidInput: '输入数据格式不正确',
|
||||
requiredFieldMissing: '必填字段不能为空',
|
||||
fieldTooLong: '输入内容超出长度限制',
|
||||
fieldTooShort: '输入内容长度不足',
|
||||
invalidDate: '日期格式不正确',
|
||||
invalidNumber: '数字格式不正确',
|
||||
|
||||
// 操作相关错误
|
||||
operationFailed: '操作失败,请稍后重试',
|
||||
saveSuccess: '保存成功',
|
||||
saveFailed: '保存失败,请检查输入信息',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteFailed: '删除失败,请稍后重试',
|
||||
updateSuccess: '更新成功',
|
||||
updateFailed: '更新失败,请检查输入信息',
|
||||
|
||||
// 系统相关错误
|
||||
systemMaintenance: '系统正在维护中,请稍后访问',
|
||||
serviceUnavailable: '服务暂时不可用,请稍后重试',
|
||||
databaseError: '数据库连接错误,请联系技术支持',
|
||||
configError: '系统配置错误,请联系管理员',
|
||||
},
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { i18n } from './locales'
|
||||
import { checkEnvironmentVariables, validateEnvironment } from './utils/hertz_env'
|
||||
import './styles/index.scss'
|
||||
|
||||
// 导入Ant Design Vue
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
|
||||
// 开发环境检查
|
||||
if (import.meta.env.DEV) {
|
||||
checkEnvironmentVariables()
|
||||
validateEnvironment()
|
||||
}
|
||||
|
||||
// 创建Vue应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 使用Pinia状态管理
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
// 使用路由
|
||||
app.use(router)
|
||||
|
||||
// 使用国际化
|
||||
app.use(i18n)
|
||||
|
||||
// 初始化应用设置
|
||||
import { useAppStore } from './stores/hertz_app'
|
||||
const appStore = useAppStore()
|
||||
appStore.initAppSettings()
|
||||
|
||||
// 检查用户认证状态
|
||||
import { useUserStore } from './stores/hertz_user'
|
||||
const userStore = useUserStore()
|
||||
userStore.checkAuth()
|
||||
|
||||
// 初始化主题(必须在挂载前加载)
|
||||
import { useThemeStore } from './stores/hertz_theme'
|
||||
const themeStore = useThemeStore()
|
||||
themeStore.loadTheme()
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
@@ -1,69 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 会话与消息类型
|
||||
export interface AIChatItem {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
latest_message?: string
|
||||
}
|
||||
|
||||
export interface AIChatDetail {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AIChatMessage {
|
||||
id: number
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ChatListData {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
chats: AIChatItem[]
|
||||
}
|
||||
|
||||
export interface ChatDetailData {
|
||||
chat: AIChatDetail
|
||||
messages: AIChatMessage[]
|
||||
}
|
||||
|
||||
export interface SendMessageData {
|
||||
user_message: AIChatMessage
|
||||
ai_message: AIChatMessage
|
||||
}
|
||||
|
||||
export const aiApi = {
|
||||
listChats: (params?: { query?: string; page?: number; page_size?: number }): Promise<ApiResponse<ChatListData>> =>
|
||||
request.get('/api/ai/chats/', { params, showError: false }),
|
||||
|
||||
createChat: (body?: { title?: string }): Promise<ApiResponse<AIChatDetail>> =>
|
||||
request.post('/api/ai/chats/create/', body || { title: '新对话' }),
|
||||
|
||||
getChatDetail: (chatId: number): Promise<ApiResponse<ChatDetailData>> =>
|
||||
request.get(`/api/ai/chats/${chatId}/`),
|
||||
|
||||
updateChat: (chatId: number, body: { title: string }): Promise<ApiResponse<null>> =>
|
||||
request.put(`/api/ai/chats/${chatId}/update/`, body),
|
||||
|
||||
deleteChats: (chatIds: number[]): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/ai/chats/delete/', { chat_ids: chatIds }),
|
||||
|
||||
sendMessage: (chatId: number, body: { content: string }): Promise<ApiResponse<SendMessageData>> =>
|
||||
request.post(`/api/ai/chats/${chatId}/send/`, body),
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// 统一的菜单项配置接口
|
||||
export interface UserMenuConfig {
|
||||
key: string
|
||||
label: string
|
||||
icon?: string
|
||||
path: string
|
||||
component: string // 组件路径,相对于 @/views/user_pages/
|
||||
children?: UserMenuConfig[]
|
||||
disabled?: boolean
|
||||
meta?: {
|
||||
title?: string
|
||||
requiresAuth?: boolean
|
||||
roles?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单项接口定义(用于前端显示)
|
||||
export interface MenuItem {
|
||||
key: string
|
||||
label: string
|
||||
icon?: string
|
||||
path?: string
|
||||
children?: MenuItem[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
// 统一配置 - 同时用于菜单和路由
|
||||
export const userMenuConfigs: UserMenuConfig[] = [
|
||||
{
|
||||
key: 'dashboard',
|
||||
label: '首页',
|
||||
icon: 'DashboardOutlined',
|
||||
path: '/dashboard',
|
||||
component: 'index.vue',
|
||||
meta: { title: '用户首页', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'profile',
|
||||
label: '个人信息',
|
||||
icon: 'UserOutlined',
|
||||
path: '/user/profile',
|
||||
component: 'Profile.vue',
|
||||
meta: { title: '个人信息', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'documents',
|
||||
label: '文档管理',
|
||||
icon: 'FileTextOutlined',
|
||||
path: '/user/documents',
|
||||
component: 'Documents.vue',
|
||||
meta: { title: '文档管理', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
label: '消息中心',
|
||||
icon: 'MessageOutlined',
|
||||
path: '/user/messages',
|
||||
component: 'Messages.vue',
|
||||
meta: { title: '消息中心', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'system-monitor',
|
||||
label: '系统监控',
|
||||
icon: 'DashboardOutlined',
|
||||
path: '/user/system-monitor',
|
||||
component: 'SystemMonitor.vue',
|
||||
meta: { title: '系统监控', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'ai-chat',
|
||||
label: 'AI助手',
|
||||
icon: 'MessageOutlined',
|
||||
path: '/user/ai-chat',
|
||||
component: 'AiChat.vue',
|
||||
meta: { title: 'AI助手', requiresAuth: true }
|
||||
},
|
||||
]
|
||||
|
||||
// 显式组件映射 - 避免Vite动态导入限制
|
||||
const explicitComponentMap: Record<string, any> = {
|
||||
'index.vue': defineAsyncComponent(() => import('@/views/user_pages/index.vue')),
|
||||
'Profile.vue': defineAsyncComponent(() => import('@/views/user_pages/Profile.vue')),
|
||||
'Documents.vue': defineAsyncComponent(() => import('@/views/user_pages/Documents.vue')),
|
||||
'Messages.vue': defineAsyncComponent(() => import('@/views/user_pages/Messages.vue')),
|
||||
'SystemMonitor.vue': defineAsyncComponent(() => import('@/views/user_pages/SystemMonitor.vue')),
|
||||
'AiChat.vue': defineAsyncComponent(() => import('@/views/user_pages/AiChat.vue')),
|
||||
}
|
||||
|
||||
// 自动生成菜单项(用于前端显示)
|
||||
export const userMenuItems: MenuItem[] = userMenuConfigs.map(config => ({
|
||||
key: config.key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
path: config.path,
|
||||
disabled: config.disabled,
|
||||
children: config.children?.map(child => ({
|
||||
key: child.key,
|
||||
label: child.label,
|
||||
icon: child.icon,
|
||||
path: child.path,
|
||||
disabled: child.disabled
|
||||
}))
|
||||
}))
|
||||
|
||||
// 组件映射表 - 用于解决Vite动态导入限制
|
||||
const componentMap: Record<string, () => Promise<any>> = {
|
||||
'index.vue': () => import('@/views/user_pages/index.vue'),
|
||||
'Profile.vue': () => import('@/views/user_pages/Profile.vue'),
|
||||
'Documents.vue': () => import('@/views/user_pages/Documents.vue'),
|
||||
'Messages.vue': () => import('@/views/user_pages/Messages.vue'),
|
||||
'SystemMonitor.vue': () => import('@/views/user_pages/SystemMonitor.vue'),
|
||||
'AiChat.vue': () => import('@/views/user_pages/AiChat.vue'),
|
||||
}
|
||||
|
||||
// 自动生成路由配置
|
||||
export const userRoutes: RouteRecordRaw[] = userMenuConfigs.map(config => {
|
||||
const route: RouteRecordRaw = {
|
||||
path: config.path,
|
||||
name: `User${config.key.charAt(0).toUpperCase() + config.key.slice(1)}`,
|
||||
component: componentMap[config.component] || (() => import('@/views/NotFound.vue')),
|
||||
meta: {
|
||||
title: config.meta?.title || config.label,
|
||||
requiresAuth: config.meta?.requiresAuth ?? true,
|
||||
...config.meta
|
||||
}
|
||||
}
|
||||
|
||||
if (config.children && config.children.length > 0) {
|
||||
route.children = config.children.map(child => ({
|
||||
path: child.path,
|
||||
name: `User${child.key.charAt(0).toUpperCase() + child.key.slice(1)}`,
|
||||
component: componentMap[child.component] || (() => import('@/views/NotFound.vue')),
|
||||
meta: {
|
||||
title: child.meta?.title || child.label,
|
||||
requiresAuth: child.meta?.requiresAuth ?? true,
|
||||
...child.meta
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return route
|
||||
})
|
||||
|
||||
// 根据菜单项生成路由路径
|
||||
export function getMenuPath(menuKey: string): string {
|
||||
const findPath = (items: MenuItem[], key: string): string | null => {
|
||||
for (const item of items) {
|
||||
if (item.key === key && item.path) return item.path
|
||||
if (item.children) {
|
||||
const childPath = findPath(item.children, key)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findPath(userMenuItems, menuKey) || '/dashboard'
|
||||
}
|
||||
|
||||
// 获取菜单的面包屑路径
|
||||
export function getMenuBreadcrumb(menuKey: string): string[] {
|
||||
const findBreadcrumb = (items: MenuItem[], key: string, path: string[] = []): string[] | null => {
|
||||
for (const item of items) {
|
||||
const currentPath = [...path, item.label]
|
||||
if (item.key === menuKey) return currentPath
|
||||
if (item.children) {
|
||||
const childPath = findBreadcrumb(item.children, key, currentPath)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findBreadcrumb(userMenuItems, menuKey) || ['仪表盘']
|
||||
}
|
||||
|
||||
// 自动生成组件映射(基于配置和显式映射)
|
||||
export const generateComponentMap = () => {
|
||||
const map: Record<string, any> = {}
|
||||
const processConfigs = (configs: UserMenuConfig[]) => {
|
||||
configs.forEach(config => {
|
||||
if (explicitComponentMap[config.component]) {
|
||||
map[config.key] = explicitComponentMap[config.component]
|
||||
} else {
|
||||
map[config.key] = defineAsyncComponent(() => import('@/views/NotFound.vue'))
|
||||
}
|
||||
if (config.children) processConfigs(config.children)
|
||||
})
|
||||
}
|
||||
processConfigs(userMenuConfigs)
|
||||
return map
|
||||
}
|
||||
|
||||
// 导出自动生成的组件映射
|
||||
export const userComponentMap = generateComponentMap()
|
||||
|
||||
// 根据用户权限过滤菜单项
|
||||
export const getFilteredUserMenuItems = (userRoles: string[], userPermissions: string[]): MenuItem[] => {
|
||||
return userMenuConfigs
|
||||
.filter(config => {
|
||||
if (!config.meta?.roles || config.meta.roles.length === 0) return true
|
||||
return config.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
})
|
||||
.map(config => ({
|
||||
key: config.key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
path: config.path,
|
||||
disabled: config.disabled,
|
||||
children: config.children?.filter(child => {
|
||||
if (!child.meta?.roles || child.meta.roles.length === 0) return true
|
||||
return child.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
}).map(child => ({
|
||||
key: child.key,
|
||||
label: child.label,
|
||||
icon: child.icon,
|
||||
path: child.path,
|
||||
disabled: child.disabled
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
// 检查用户是否有访问特定菜单的权限
|
||||
export const hasUserMenuPermission = (menuKey: string, userRoles: string[]): boolean => {
|
||||
const menuConfig = userMenuConfigs.find(config => config.key === menuKey)
|
||||
if (!menuConfig) return false
|
||||
if (!menuConfig.meta?.roles || menuConfig.meta.roles.length === 0) return true
|
||||
return menuConfig.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
<template>
|
||||
<div class="ai-chat-page">
|
||||
<a-page-header title="AI助手" sub-title="与 AI 进行智能对话">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input-search v-model:value="query" placeholder="搜索会话标题" style="width: 240px" @search="fetchChats" />
|
||||
<a-button @click="fetchChats" :loading="loadingChats">
|
||||
<ReloadOutlined />
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="createChat" :loading="creating">
|
||||
<PlusOutlined />
|
||||
新建对话
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧:会话列表 -->
|
||||
<a-col :xs="24" :md="8" :lg="6">
|
||||
<a-card title="我的对话" bordered>
|
||||
<a-list
|
||||
:data-source="chatList"
|
||||
item-layout="horizontal"
|
||||
:loading="loadingChats"
|
||||
:pagination="{ pageSize: pageSize, total: total, current: page, onChange: onPageChange }"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item :class="{ active: item.id === currentChatId }" @click="selectChat(item.id)">
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div class="chat-title-row">
|
||||
<span class="chat-title">{{ item.title }}</span>
|
||||
<a-space>
|
||||
<a-button size="small" type="text" @click.stop="openRename(item)">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
<a-popconfirm title="确认删除该对话?" @confirm="deleteChat(item.id)">
|
||||
<a-button size="small" danger type="text" @click.stop>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="chat-desc">{{ item.latest_message || '暂无消息' }}</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧:消息区 -->
|
||||
<a-col :xs="24" :md="16" :lg="18">
|
||||
<a-card :title="currentChat?.title || '请选择或新建对话'" bordered class="chat-card">
|
||||
<div class="messages" ref="messagesEl">
|
||||
<template v-if="messages.length">
|
||||
<div v-for="m in messages" :key="m.id" :class="['msg', m.role]">
|
||||
<div class="bubble">
|
||||
<div class="content" v-html="renderContent(m.content)"></div>
|
||||
<div class="time">{{ formatTime(m.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-empty v-else description="暂无消息" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="composer">
|
||||
<a-textarea v-model:value="input" :rows="3" placeholder="输入你的问题..." :disabled="!currentChatId" />
|
||||
<a-space style="margin-top: 8px;">
|
||||
<a-button type="primary" :disabled="!canSend" :loading="sending" @click="send">
|
||||
<SendOutlined />
|
||||
发送
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 重命名对话 -->
|
||||
<a-modal v-model:open="renameOpen" title="重命名对话" @ok="doRename" :confirm-loading="renaming">
|
||||
<a-input v-model:value="renameTitle" placeholder="请输入新标题" />
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, SendOutlined } from '@ant-design/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { aiApi, type AIChatItem, type AIChatDetail, type AIChatMessage } from '@/api/ai'
|
||||
|
||||
// 会话列表状态
|
||||
const chatList = ref<AIChatItem[]>([])
|
||||
const query = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const loadingChats = ref(false)
|
||||
const creating = ref(false)
|
||||
|
||||
// 当前会话与消息
|
||||
const currentChatId = ref<number | null>(null)
|
||||
const currentChat = ref<AIChatDetail | null>(null)
|
||||
const messages = ref<AIChatMessage[]>([])
|
||||
const loadingMessages = ref(false)
|
||||
const sending = ref(false)
|
||||
|
||||
// 重命名
|
||||
const renameOpen = ref(false)
|
||||
const renameTitle = ref('')
|
||||
const renaming = ref(false)
|
||||
let renameTargetId: number | null = null
|
||||
|
||||
const messagesEl = ref<HTMLElement | null>(null)
|
||||
|
||||
const canSend = computed(() => !!currentChatId.value && !!input.value.trim())
|
||||
const input = ref('')
|
||||
|
||||
const fetchChats = async () => {
|
||||
loadingChats.value = true
|
||||
try {
|
||||
const res = await aiApi.listChats({ query: query.value || undefined, page: page.value, page_size: pageSize.value })
|
||||
if (res.success) {
|
||||
chatList.value = res.data.chats || []
|
||||
total.value = res.data.total || 0
|
||||
// 保持选择
|
||||
if (!currentChatId.value && chatList.value.length) selectChat(chatList.value[0].id)
|
||||
} else {
|
||||
message.error(res.message || '获取对话列表失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 403) {
|
||||
message.warning('暂无权限访问AI助手,请联系管理员开通权限')
|
||||
} else {
|
||||
message.error(e?.message || '网络错误')
|
||||
}
|
||||
} finally {
|
||||
loadingChats.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onPageChange = (p: number) => { page.value = p; fetchChats() }
|
||||
|
||||
const selectChat = async (id: number) => {
|
||||
if (currentChatId.value === id && messages.value.length) return
|
||||
currentChatId.value = id
|
||||
loadingMessages.value = true
|
||||
try {
|
||||
const res = await aiApi.getChatDetail(id)
|
||||
if (res.success) {
|
||||
currentChat.value = res.data.chat
|
||||
messages.value = res.data.messages || []
|
||||
await nextTick(); scrollToBottom()
|
||||
} else {
|
||||
message.error(res.message || '获取会话详情失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '网络错误')
|
||||
} finally {
|
||||
loadingMessages.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createChat = async () => {
|
||||
creating.value = true
|
||||
try {
|
||||
const res = await aiApi.createChat({ title: '新对话' })
|
||||
if (res.success) {
|
||||
message.success('创建成功')
|
||||
await fetchChats()
|
||||
selectChat(res.data.id)
|
||||
} else {
|
||||
message.error(res.message || '创建失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '网络错误')
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openRename = (item: AIChatItem) => {
|
||||
renameTargetId = item.id
|
||||
renameTitle.value = item.title
|
||||
renameOpen.value = true
|
||||
}
|
||||
|
||||
const doRename = async () => {
|
||||
if (!renameTargetId || !renameTitle.value.trim()) { message.warning('标题不能为空'); return }
|
||||
renaming.value = true
|
||||
try {
|
||||
const res = await aiApi.updateChat(renameTargetId, { title: renameTitle.value.trim() })
|
||||
if (res.success) { message.success('重命名成功'); renameOpen.value = false; await fetchChats() }
|
||||
else { message.error(res.message || '重命名失败') }
|
||||
} catch (e: any) { message.error(e?.message || '网络错误') }
|
||||
finally { renaming.value = false }
|
||||
}
|
||||
|
||||
const deleteChat = async (id: number) => {
|
||||
try {
|
||||
const res = await aiApi.deleteChats([id])
|
||||
if (res.success) {
|
||||
message.success('删除成功')
|
||||
await fetchChats()
|
||||
if (currentChatId.value === id) { currentChatId.value = null; currentChat.value = null; messages.value = [] }
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (e: any) { message.error(e?.message || '网络错误') }
|
||||
}
|
||||
|
||||
const send = async () => {
|
||||
if (!canSend.value || !currentChatId.value) return
|
||||
const content = input.value.trim()
|
||||
sending.value = true
|
||||
try {
|
||||
const res = await aiApi.sendMessage(currentChatId.value, { content })
|
||||
if (res.success) {
|
||||
messages.value.push(res.data.user_message, res.data.ai_message)
|
||||
input.value = ''
|
||||
await nextTick(); scrollToBottom(); fetchChats() // 更新列表预览
|
||||
} else {
|
||||
message.error(res.message || '发送失败')
|
||||
}
|
||||
} catch (e: any) { message.error(e?.message || '网络错误') }
|
||||
finally { sending.value = false }
|
||||
}
|
||||
|
||||
const formatTime = (t: string) => dayjs(t).format('YYYY-MM-DD HH:mm:ss')
|
||||
const renderContent = (c: string) => c.replace(/\n/g, '<br/>')
|
||||
const scrollToBottom = () => { const el = messagesEl.value; if (el) el.scrollTop = el.scrollHeight }
|
||||
|
||||
onMounted(() => { fetchChats() })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-chat-page { padding: 16px; }
|
||||
.chat-title-row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.chat-desc { color: #666; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.messages { min-height: 420px; max-height: 60vh; overflow-y: auto; padding: 8px; background: #fafafa; border-radius: 8px; }
|
||||
.msg { display: flex; margin-bottom: 12px; }
|
||||
.msg .bubble { max-width: 80%; padding: 10px 12px; border-radius: 8px; position: relative; }
|
||||
.msg .time { margin-top: 6px; font-size: 12px; color: #999; }
|
||||
.msg.user { justify-content: flex-end; }
|
||||
.msg.user .bubble { background: #e6f7ff; }
|
||||
.msg.assistant { justify-content: flex-start; }
|
||||
.msg.assistant .bubble { background: #f6ffed; }
|
||||
.composer { margin-top: 8px; }
|
||||
.chat-card :deep(.ant-card-head) { background: #fff; }
|
||||
.active { background: #f0f7ff; }
|
||||
</style>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,424 +0,0 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
// 角色权限枚举
|
||||
export enum UserRole {
|
||||
ADMIN = 'admin',
|
||||
SYSTEM_ADMIN = 'system_admin',
|
||||
NORMAL_USER = 'normal_user',
|
||||
SUPER_ADMIN = 'super_admin'
|
||||
}
|
||||
|
||||
// 统一菜单配置接口 - 只需要在这里配置一次
|
||||
export interface AdminMenuItem {
|
||||
key: string; // 菜单唯一标识
|
||||
title: string; // 菜单标题
|
||||
icon?: string; // 菜单图标
|
||||
path: string; // 路由路径
|
||||
component: string; // 组件路径(相对于@/views/admin_page/)
|
||||
isDefault?: boolean; // 是否为默认路由(首页)
|
||||
roles?: UserRole[]; // 允许访问的角色,不设置则使用默认管理员角色
|
||||
permission?: string; // 所需权限标识符
|
||||
children?: AdminMenuItem[]; // 子菜单
|
||||
}
|
||||
|
||||
// 🎯 统一配置中心 - 只需要在这里修改菜单配置
|
||||
export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
{
|
||||
key: "dashboard",
|
||||
title: "仪表盘",
|
||||
icon: "DashboardOutlined",
|
||||
path: "/admin",
|
||||
component: "Dashboard.vue",
|
||||
isDefault: true, // 标记为默认首页
|
||||
},
|
||||
{
|
||||
key: "user-management",
|
||||
title: "用户管理",
|
||||
icon: "UserOutlined",
|
||||
path: "/admin/user-management",
|
||||
component: "UserManagement.vue",
|
||||
permission: "system:user:list", // 需要用户列表权限
|
||||
},
|
||||
{
|
||||
key: "department-management",
|
||||
title: "部门管理",
|
||||
icon: "SettingOutlined",
|
||||
path: "/admin/department-management",
|
||||
component: "DepartmentManagement.vue",
|
||||
permission: "system:dept:list", // 需要部门列表权限
|
||||
},
|
||||
{
|
||||
key: "menu-management",
|
||||
title: "菜单管理",
|
||||
icon: "SettingOutlined",
|
||||
path: "/admin/menu-management",
|
||||
component: "MenuManagement.vue",
|
||||
permission: "system:menu:list", // 需要菜单列表权限
|
||||
},
|
||||
{
|
||||
key: "teacher",
|
||||
title: "角色管理",
|
||||
icon: "UserOutlined",
|
||||
path: "/admin/teacher",
|
||||
component: "Role.vue",
|
||||
permission: "system:role:list", // 需要角色列表权限
|
||||
},
|
||||
{
|
||||
key: "notification-management",
|
||||
title: "通知管理",
|
||||
icon: "UserOutlined",
|
||||
path: "/admin/notification-management",
|
||||
component: "NotificationManagement.vue",
|
||||
permission: "studio:notice:list", // 需要通知列表权限
|
||||
},
|
||||
{
|
||||
key: "log-management",
|
||||
title: "日志管理",
|
||||
icon: "FileSearchOutlined",
|
||||
path: "/admin/log-management",
|
||||
component: "LogManagement.vue",
|
||||
permission: "log.view_operationlog", // 查看操作日志权限
|
||||
},
|
||||
{
|
||||
key: "knowledge-base",
|
||||
title: "知识库管理",
|
||||
icon: "DatabaseOutlined",
|
||||
path: "/admin/knowledge-base",
|
||||
component: "KnowledgeBaseManagement.vue",
|
||||
// 菜单访问权限:需要具备文章列表权限
|
||||
permission: "system:knowledge:article:list",
|
||||
},
|
||||
{
|
||||
key: "yolo-model",
|
||||
title: "YOLO模型",
|
||||
icon: "ClusterOutlined",
|
||||
path: "/admin/yolo-model",
|
||||
component: "ModelManagement.vue", // 默认显示模型管理页面
|
||||
// 父菜单不设置权限,由子菜单的权限决定是否显示
|
||||
children: [
|
||||
{
|
||||
key: "model-management",
|
||||
title: "模型管理",
|
||||
icon: "RobotOutlined",
|
||||
path: "/admin/model-management",
|
||||
component: "ModelManagement.vue",
|
||||
permission: "system:yolo:model:list",
|
||||
},
|
||||
{
|
||||
key: "alert-level-management",
|
||||
title: "模型类别管理",
|
||||
icon: "WarningOutlined",
|
||||
path: "/admin/alert-level-management",
|
||||
component: "AlertLevelManagement.vue",
|
||||
permission: "system:yolo:alert:list",
|
||||
},
|
||||
{
|
||||
key: "alert-processing-center",
|
||||
title: "告警处理中心",
|
||||
icon: "BellOutlined",
|
||||
path: "/admin/alert-processing-center",
|
||||
component: "AlertProcessingCenter.vue",
|
||||
permission: "system:yolo:alert:process",
|
||||
},
|
||||
{
|
||||
key: "detection-history-management",
|
||||
title: "检测历史管理",
|
||||
icon: "HistoryOutlined",
|
||||
path: "/admin/detection-history-management",
|
||||
component: "DetectionHistoryManagement.vue",
|
||||
permission: "system:yolo:history:list",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 默认管理员角色 - 修改为空数组,通过自定义权限检查函数处理
|
||||
const DEFAULT_ADMIN_ROLES: UserRole[] = [];
|
||||
|
||||
// 组件映射 - 静态导入以支持Vite分析
|
||||
const COMPONENT_MAP: { [key: string]: () => Promise<any> } = {
|
||||
'Dashboard.vue': () => import("@/views/admin_page/Dashboard.vue"),
|
||||
'UserManagement.vue': () => import("@/views/admin_page/UserManagement.vue"),
|
||||
'DepartmentManagement.vue': () => import("@/views/admin_page/DepartmentManagement.vue"),
|
||||
'Role.vue': () => import("@/views/admin_page/Role.vue"),
|
||||
'MenuManagement.vue': () => import("@/views/admin_page/MenuManagement.vue"),
|
||||
'NotificationManagement.vue': () => import("@/views/admin_page/NotificationManagement.vue"),
|
||||
'LogManagement.vue': () => import("@/views/admin_page/LogManagement.vue"),
|
||||
'KnowledgeBaseManagement.vue': () => import("@/views/admin_page/KnowledgeBaseManagement.vue"),
|
||||
'ModelManagement.vue': () => import("@/views/admin_page/ModelManagement.vue"),
|
||||
'AlertLevelManagement.vue': () => import("@/views/admin_page/AlertLevelManagement.vue"),
|
||||
'AlertProcessingCenter.vue': () => import("@/views/admin_page/AlertProcessingCenter.vue"),
|
||||
'DetectionHistoryManagement.vue': () => import("@/views/admin_page/DetectionHistoryManagement.vue"),
|
||||
};
|
||||
|
||||
// 🚀 自动生成路由配置
|
||||
function generateAdminRoutes(): RouteRecordRaw {
|
||||
const children: RouteRecordRaw[] = [];
|
||||
|
||||
ADMIN_MENU_CONFIG.forEach(item => {
|
||||
// 如果有子菜单,将子菜单作为独立的路由项
|
||||
if (item.children && item.children.length > 0) {
|
||||
// 为每个子菜单创建独立的路由
|
||||
item.children.forEach(child => {
|
||||
children.push({
|
||||
path: child.path.replace("/admin/", ""),
|
||||
name: child.key,
|
||||
component: COMPONENT_MAP[child.component] || (() => import("@/views/admin_page/Dashboard.vue")),
|
||||
meta: {
|
||||
title: child.title,
|
||||
requiresAuth: true,
|
||||
roles: child.roles || DEFAULT_ADMIN_ROLES,
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 没有子菜单的普通菜单项
|
||||
children.push({
|
||||
path: item.isDefault ? "" : item.path.replace("/admin/", ""),
|
||||
name: item.key,
|
||||
component: COMPONENT_MAP[item.component] || (() => import("@/views/admin_page/Dashboard.vue")),
|
||||
meta: {
|
||||
title: item.title,
|
||||
requiresAuth: true,
|
||||
roles: item.roles || DEFAULT_ADMIN_ROLES,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🛣️ 生成的管理端路由配置:', children.map(child => ({
|
||||
path: child.path,
|
||||
name: child.name,
|
||||
title: child.meta?.title
|
||||
})));
|
||||
|
||||
return {
|
||||
path: "/admin",
|
||||
name: "Admin",
|
||||
component: () => import("@/views/admin_page/index.vue"),
|
||||
meta: {
|
||||
title: "管理后台",
|
||||
requiresAuth: true,
|
||||
roles: DEFAULT_ADMIN_ROLES,
|
||||
},
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
// 🚀 自动生成菜单配置
|
||||
export interface MenuConfig {
|
||||
key: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
path: string;
|
||||
children?: MenuConfig[];
|
||||
}
|
||||
|
||||
function generateMenuConfig(): MenuConfig[] {
|
||||
return ADMIN_MENU_CONFIG.map(item => ({
|
||||
key: item.key,
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
path: item.path,
|
||||
children: item.children?.map(child => ({
|
||||
key: child.key,
|
||||
title: child.title,
|
||||
icon: child.icon,
|
||||
path: child.path,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
// 🚀 自动生成路径映射函数
|
||||
function generatePathKeyMapping(): { [path: string]: string } {
|
||||
const mapping: { [path: string]: string } = {};
|
||||
|
||||
function addToMapping(items: AdminMenuItem[], parentPath = '') {
|
||||
items.forEach(item => {
|
||||
mapping[item.path] = item.key;
|
||||
if (item.children) {
|
||||
addToMapping(item.children, item.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addToMapping(ADMIN_MENU_CONFIG);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
// 导出的配置和函数
|
||||
export const adminMenuRoutes: RouteRecordRaw = generateAdminRoutes();
|
||||
export const adminMenuConfig: MenuConfig[] = generateMenuConfig();
|
||||
|
||||
// 路径到key的映射
|
||||
const pathKeyMapping = generatePathKeyMapping();
|
||||
|
||||
// 🎯 根据路径获取菜单key - 自动生成
|
||||
export const getMenuKeyByPath = (path: string): string => {
|
||||
// 精确匹配
|
||||
if (pathKeyMapping[path]) {
|
||||
return pathKeyMapping[path];
|
||||
}
|
||||
|
||||
// 模糊匹配
|
||||
for (const [mappedPath, key] of Object.entries(pathKeyMapping)) {
|
||||
if (path.includes(mappedPath) && mappedPath !== '/admin') {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回dashboard
|
||||
return 'dashboard';
|
||||
};
|
||||
|
||||
// 🎯 根据菜单key获取路径 - 自动生成
|
||||
export const getPathByMenuKey = (key: string): string => {
|
||||
console.log('🔍 查找菜单路径:', key);
|
||||
|
||||
const menuItem = ADMIN_MENU_CONFIG.find(item => item.key === key);
|
||||
if (menuItem) {
|
||||
console.log('✅ 找到父菜单路径:', menuItem.path);
|
||||
return menuItem.path;
|
||||
}
|
||||
|
||||
// 在子菜单中查找
|
||||
for (const item of ADMIN_MENU_CONFIG) {
|
||||
if (item.children) {
|
||||
const childItem = item.children.find(child => child.key === key);
|
||||
if (childItem) {
|
||||
console.log('✅ 找到子菜单路径:', childItem.path);
|
||||
return childItem.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('❌ 未找到菜单路径,返回默认路径');
|
||||
return '/admin';
|
||||
};
|
||||
|
||||
// 🎯 根据菜单key获取标题 - 自动生成
|
||||
export const getTitleByMenuKey = (key: string): string => {
|
||||
const menuItem = ADMIN_MENU_CONFIG.find(item => item.key === key);
|
||||
if (menuItem) return menuItem.title;
|
||||
|
||||
// 在子菜单中查找
|
||||
for (const item of ADMIN_MENU_CONFIG) {
|
||||
if (item.children) {
|
||||
const childItem = item.children.find(child => child.key === key);
|
||||
if (childItem) return childItem.title;
|
||||
}
|
||||
}
|
||||
|
||||
return '仪表盘';
|
||||
};
|
||||
|
||||
// 菜单权限检查
|
||||
export const hasMenuPermission = (menuKey: string, userRole: string): boolean => {
|
||||
const menuItem = ADMIN_MENU_CONFIG.find(item => item.key === menuKey);
|
||||
if (!menuItem) return false;
|
||||
|
||||
return menuItem.roles ? menuItem.roles.includes(userRole as UserRole) : DEFAULT_ADMIN_ROLES.includes(userRole as UserRole);
|
||||
};
|
||||
|
||||
// 🎯 新增:根据用户权限过滤菜单配置
|
||||
export const getFilteredMenuConfig = (userRoles: string[], userPermissions: string[], userMenuPermissions?: number[]): MenuConfig[] => {
|
||||
const userRole = userRoles[0]; // 取第一个角色作为主要角色
|
||||
|
||||
// 仅管理员角色显示管理端菜单
|
||||
const adminRoles = ['admin', 'system_admin', 'super_admin'];
|
||||
const isAdminRole = userRoles.some(r => adminRoles.includes(r));
|
||||
if (!isAdminRole) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 对 super_admin / system_admin 开放所有管理菜单(忽略权限字符串过滤)
|
||||
const isPrivilegedAdmin = userRoles.includes('super_admin') || userRoles.includes('system_admin');
|
||||
|
||||
// 过滤菜单项 - 基于权限字符串检查
|
||||
const filteredMenus = ADMIN_MENU_CONFIG.filter(menuItem => {
|
||||
console.log(`🔍 检查菜单项: ${menuItem.title} (${menuItem.key})`, {
|
||||
hasPermission: !!menuItem.permission,
|
||||
permission: menuItem.permission,
|
||||
hasChildren: !!(menuItem.children && menuItem.children.length > 0),
|
||||
childrenCount: menuItem.children?.length || 0
|
||||
});
|
||||
|
||||
// 如果菜单没有配置权限要求,则默认允许访问(如仪表盘)
|
||||
if (!menuItem.permission) {
|
||||
console.log(`✅ 菜单 ${menuItem.title} 无权限要求,允许访问`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查用户是否有该菜单所需的权限
|
||||
const hasMenuPermission = isPrivilegedAdmin ? true : hasPermission(menuItem.permission, userPermissions);
|
||||
|
||||
if (!hasMenuPermission) {
|
||||
console.log(`❌ 菜单 ${menuItem.title} 权限不足,拒绝访问`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有子菜单,过滤子菜单
|
||||
if (menuItem.children && menuItem.children.length > 0) {
|
||||
const filteredChildren = menuItem.children.filter(child => {
|
||||
// 如果子菜单没有配置权限要求,则默认允许访问
|
||||
if (!child.permission) {
|
||||
console.log(`✅ 子菜单 ${child.title} 无权限要求,允许访问`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const childHasPermission = hasPermission(child.permission, userPermissions);
|
||||
console.log(`🔍 子菜单 ${child.title} 权限检查:`, {
|
||||
permission: child.permission,
|
||||
hasPermission: childHasPermission
|
||||
});
|
||||
return childHasPermission;
|
||||
});
|
||||
|
||||
console.log(`📊 菜单 ${menuItem.title} 子菜单过滤结果:`, {
|
||||
originalCount: menuItem.children.length,
|
||||
filteredCount: filteredChildren.length,
|
||||
filteredChildren: filteredChildren.map(c => c.title)
|
||||
});
|
||||
|
||||
// 如果没有任何子菜单有权限,则不显示父菜单
|
||||
if (filteredChildren.length === 0) {
|
||||
console.log(`❌ 菜单 ${menuItem.title} 所有子菜单都无权限,隐藏父菜单`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新子菜单列表
|
||||
menuItem.children = filteredChildren;
|
||||
}
|
||||
|
||||
console.log(`✅ 菜单 ${menuItem.title} 通过权限检查`);
|
||||
return true;
|
||||
}).map(menuItem => ({
|
||||
key: menuItem.key,
|
||||
title: menuItem.title,
|
||||
icon: menuItem.icon,
|
||||
path: menuItem.path,
|
||||
children: menuItem.children?.map(child => ({
|
||||
key: child.key,
|
||||
title: child.title,
|
||||
icon: child.icon,
|
||||
path: child.path
|
||||
}))
|
||||
}));
|
||||
|
||||
return filteredMenus;
|
||||
};
|
||||
|
||||
// 🎯 新增:检查用户是否有任何管理员菜单权限
|
||||
// 修改逻辑:只有normal_user角色不能访问管理端,其他所有角色都可以访问
|
||||
export const hasAnyAdminPermission = (userRoles: string[]): boolean => {
|
||||
// 仅当包含 admin/system_admin/super_admin 之一才视为管理员
|
||||
const adminRoles = ['admin', 'system_admin', 'super_admin'];
|
||||
return userRoles.some(role => adminRoles.includes(role));
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定权限
|
||||
*/
|
||||
const hasPermission = (permission: string, userPermissions: string[]): boolean => {
|
||||
return userPermissions.includes(permission);
|
||||
};
|
||||
@@ -1,275 +0,0 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { useUserStore } from "@/stores/hertz_user";
|
||||
import { adminMenuRoutes, UserRole } from "./admin_menu";
|
||||
import { userRoutes } from "./user_menu_ai";
|
||||
|
||||
// 固定路由配置
|
||||
const fixedRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: () => import("@/views/Home.vue"),
|
||||
meta: {
|
||||
title: "首页",
|
||||
requiresAuth: false,
|
||||
},
|
||||
children: [...generateDynamicRoutes("public")],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: () => import("@/views/Login.vue"),
|
||||
meta: {
|
||||
title: "登录",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "Register",
|
||||
component: () => import("@/views/register.vue"),
|
||||
meta: {
|
||||
title: "注册",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
// 管理端路由 - 从admin_menu.ts导入
|
||||
adminMenuRoutes,
|
||||
];
|
||||
|
||||
// 动态生成路由配置
|
||||
function generateDynamicRoutes(targetDir: string = ""): RouteRecordRaw[] {
|
||||
if (!targetDir) {
|
||||
return [];
|
||||
}
|
||||
const viewsContext = import.meta.glob("@/views/**/*.vue", { eager: true });
|
||||
|
||||
return Object.entries(viewsContext)
|
||||
.map(([path, component]) => {
|
||||
const relativePath = path.match(/\/views\/(.+?)\.vue$/)?.[1];
|
||||
if (!relativePath) return null;
|
||||
|
||||
const fileName = relativePath.replace(".vue", "");
|
||||
const routeName = fileName.split("/").pop()!;
|
||||
|
||||
// 过滤条件
|
||||
if (targetDir && !fileName.startsWith(targetDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成路径和标题
|
||||
const routePath = `/${fileName.replace(/([A-Z])/g, "$1").toLowerCase()}`;
|
||||
const requiresAuth =
|
||||
(!routePath.startsWith("/demo") && !routePath.startsWith("/public")) || routePath.startsWith("/user_pages")&& routePath.startsWith("/admin_page");
|
||||
const pageTitle = (component as any)?.default?.title;
|
||||
|
||||
// 根据路径设置角色权限
|
||||
let roles: UserRole[] = [];
|
||||
if (routePath.startsWith("/admin_page")) {
|
||||
roles = [UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
|
||||
} else if (routePath.startsWith("/user_pages")) {
|
||||
roles = [UserRole.NORMAL_USER, UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
|
||||
} else if (routePath.startsWith("/demo")) {
|
||||
roles = []; // demo页面不需要特定角色
|
||||
}
|
||||
|
||||
return {
|
||||
path: routePath,
|
||||
name: routeName,
|
||||
component: () => import(/* @vite-ignore */ path),
|
||||
meta: {
|
||||
title: pageTitle,
|
||||
requiresAuth,
|
||||
roles: requiresAuth ? roles : []
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as RouteRecordRaw[];
|
||||
}
|
||||
|
||||
// 合并固定路由和动态路由
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...fixedRoutes,
|
||||
...userRoutes, // 用户菜单路由 - 现在通过统一配置自动生成
|
||||
...generateDynamicRoutes("demo"), // 生成demo文件夹的路由
|
||||
...generateDynamicRoutes("admin_page"),//生成admin_page文件夹的路由
|
||||
// 404页面始终放在最后
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "NotFound",
|
||||
component: () => import("@/views/NotFound.vue"),
|
||||
meta: {
|
||||
title: "页面未找到",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(_to, _from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
} else {
|
||||
return { top: 0 };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 递归打印路由信息
|
||||
function printRoute(route: RouteRecordRaw, level: number = 0) {
|
||||
const indent = " ".repeat(level);
|
||||
const icon = route.meta.requiresAuth ? "🔒" : "🔓";
|
||||
const auth = route.meta.requiresAuth ? "需要登录" : "公开访问";
|
||||
console.log(`${indent}${icon} ${route.path} → ${route.meta.title} (${auth})`);
|
||||
|
||||
// 递归打印子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children.forEach((child) => printRoute(child, level + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// 路由调试信息
|
||||
function logRouteInfo() {
|
||||
console.log("🚀 管理系统 路由配置:");
|
||||
console.log("📋 路由列表:");
|
||||
|
||||
routes.forEach((route) => printRoute(route));
|
||||
|
||||
console.log(" ❓ /:pathMatch(.*)* → NotFound (页面未找到)");
|
||||
console.log("✅ 路由配置完成!");
|
||||
}
|
||||
|
||||
// 重定向计数器,防止无限重定向
|
||||
let redirectCount = 0;
|
||||
const MAX_REDIRECTS = 3;
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 调试信息
|
||||
console.log('🛡️ 路由守卫检查');
|
||||
console.log('📍 目标路由:', to.path, to.name);
|
||||
console.log('🔐 需要认证:', to.meta.requiresAuth);
|
||||
console.log('👤 用户登录状态:', userStore.isLoggedIn);
|
||||
console.log('🎫 Token:', userStore.token ? '存在' : '不存在');
|
||||
console.log('📋 用户信息:', userStore.userInfo);
|
||||
console.log('🔄 重定向计数:', redirectCount);
|
||||
|
||||
// 设置页面标题
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - 管理系统`;
|
||||
}
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
|
||||
console.log('❌ 需要登录但用户未登录,重定向到登录页');
|
||||
redirectCount++;
|
||||
if (redirectCount > MAX_REDIRECTS) {
|
||||
console.log('⚠️ 重定向次数过多,强制跳转到首页');
|
||||
redirectCount = 0;
|
||||
next({ name: "Home" });
|
||||
return;
|
||||
}
|
||||
next({ name: "Login", query: { redirect: to.fullPath } });
|
||||
return;
|
||||
}
|
||||
|
||||
// 已登录用户访问登录页,根据角色重定向到对应首页
|
||||
if (to.name === "Login" && userStore.isLoggedIn) {
|
||||
const userRole = userStore.userInfo?.roles?.[0]?.role_code;
|
||||
console.log('🔄 路由守卫 - 已登录用户访问登录页');
|
||||
console.log('👤 当前用户角色:', userRole);
|
||||
console.log('📋 用户信息:', userStore.userInfo);
|
||||
|
||||
// 重置重定向计数器
|
||||
redirectCount = 0;
|
||||
|
||||
// 仅管理员角色进入管理端,其余(含未定义)进入用户端
|
||||
const adminRoles = [UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
|
||||
const isAdmin = adminRoles.includes(userRole as UserRole);
|
||||
if (isAdmin) {
|
||||
console.log('➡️ 重定向到管理端首页');
|
||||
next({ name: "Admin" });
|
||||
} else {
|
||||
console.log('➡️ 重定向到用户端首页');
|
||||
next({ name: "UserDashboard" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if (to.meta.requiresAuth && to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const userRole = userStore.userInfo?.roles?.[0]?.role_code;
|
||||
|
||||
// 特殊处理:如果是管理端路由,使用自定义权限检查
|
||||
let hasPermission = false;
|
||||
if (to.path.startsWith('/admin')) {
|
||||
// 管理端路由:仅 admin/system_admin/super_admin 可访问
|
||||
const adminRoles = [UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
|
||||
hasPermission = adminRoles.includes(userRole as UserRole);
|
||||
} else {
|
||||
// 其他路由:使用原有的角色检查逻辑
|
||||
hasPermission = to.meta.roles.length === 0 || to.meta.roles.includes(userRole as UserRole);
|
||||
}
|
||||
|
||||
console.log('🔐 路由权限检查');
|
||||
console.log('📍 目标路由:', to.path, to.name);
|
||||
console.log('🎭 需要的角色:', to.meta.roles);
|
||||
console.log('👤 用户角色:', userRole);
|
||||
console.log('🏢 是否为管理端路由:', to.path.startsWith('/admin'));
|
||||
console.log('✅ 是否有权限:', hasPermission);
|
||||
|
||||
if (!hasPermission) {
|
||||
console.log('❌ 权限不足,准备重定向');
|
||||
|
||||
// 增加重定向计数
|
||||
redirectCount++;
|
||||
|
||||
// 防止无限重定向
|
||||
if (redirectCount > MAX_REDIRECTS) {
|
||||
console.log('⚠️ 重定向次数过多,强制跳转到首页');
|
||||
redirectCount = 0;
|
||||
next({ name: "Home" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止无限重定向:检查是否已经在重定向过程中
|
||||
if (to.name === 'Admin' || to.name === 'UserDashboard') {
|
||||
console.log('⚠️ 检测到重定向循环,强制跳转到首页');
|
||||
redirectCount = 0;
|
||||
next({ name: "Home" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有权限,根据用户角色重定向到对应首页
|
||||
// 只有normal_user角色跳转到用户端,其他角色(包括未定义的)都跳转到管理端
|
||||
if (userRole === 'normal_user') {
|
||||
console.log('➡️ 重定向到用户端首页');
|
||||
next({ name: "UserDashboard" });
|
||||
} else {
|
||||
console.log('➡️ 重定向到管理端首页 (角色:', userRole || '未定义', ')');
|
||||
next({ name: "Admin" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 成功通过所有检查,重置重定向计数器
|
||||
redirectCount = 0;
|
||||
next();
|
||||
});
|
||||
|
||||
// 路由错误处理
|
||||
router.onError((error) => {
|
||||
console.error("路由错误:", error);
|
||||
});
|
||||
|
||||
// 输出路由信息
|
||||
logRouteInfo();
|
||||
|
||||
export default router;
|
||||
@@ -1,183 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
export interface UserMenuConfig {
|
||||
key: string
|
||||
label: string
|
||||
icon?: string
|
||||
path: string
|
||||
component: string
|
||||
children?: UserMenuConfig[]
|
||||
disabled?: boolean
|
||||
meta?: {
|
||||
title?: string
|
||||
requiresAuth?: boolean
|
||||
roles?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
key: string
|
||||
label: string
|
||||
icon?: string
|
||||
path?: string
|
||||
children?: MenuItem[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const userMenuConfigs: UserMenuConfig[] = [
|
||||
{ key: 'dashboard', label: '首页', icon: 'DashboardOutlined', path: '/dashboard', component: 'index.vue', meta: { title: '用户首页', requiresAuth: true } },
|
||||
{ key: 'profile', label: '个人信息', icon: 'UserOutlined', path: '/user/profile', component: 'Profile.vue', meta: { title: '个人信息', requiresAuth: true, hideInMenu: true } },
|
||||
// { key: 'documents', label: '文档管理', icon: 'FileTextOutlined', path: '/user/documents', component: 'Documents.vue', meta: { title: '文档管理', requiresAuth: true } },
|
||||
{ key: 'system-monitor', label: '系统监控', icon: 'DashboardOutlined', path: '/user/system-monitor', component: 'SystemMonitor.vue', meta: { title: '系统监控', requiresAuth: true } },
|
||||
{ key: 'ai-chat', label: 'AI助手', icon: 'MessageOutlined', path: '/user/ai-chat', component: 'AiChat.vue', meta: { title: 'AI助手', requiresAuth: true } },
|
||||
{ key: 'yolo-detection', label: 'YOLO检测', icon: 'ScanOutlined', path: '/user/yolo-detection', component: 'YoloDetection.vue', meta: { title: 'YOLO检测中心', requiresAuth: true } },
|
||||
{ key: 'live-detection', label: '实时检测', icon: 'VideoCameraOutlined', path: '/user/live-detection', component: 'LiveDetection.vue', meta: { title: '实时检测', requiresAuth: true } },
|
||||
{ key: 'detection-history', label: '检测历史', icon: 'HistoryOutlined', path: '/user/detection-history', component: 'DetectionHistory.vue', meta: { title: '检测历史记录', requiresAuth: true } },
|
||||
{ key: 'alert-center', label: '告警中心', icon: 'ExclamationCircleOutlined', path: '/user/alert-center', component: 'AlertCenter.vue', meta: { title: '告警中心', requiresAuth: true } },
|
||||
{ key: 'notice-center', label: '通知中心', icon: 'BellOutlined', path: '/user/notice', component: 'NoticeCenter.vue', meta: { title: '通知中心', requiresAuth: true } },
|
||||
{ key: 'knowledge-center', label: '知识库中心', icon: 'DatabaseOutlined', path: '/user/knowledge', component: 'KnowledgeCenter.vue', meta: { title: '知识库中心', requiresAuth: true } },
|
||||
]
|
||||
|
||||
const explicitComponentMap: Record<string, any> = {
|
||||
'index.vue': defineAsyncComponent(() => import('@/views/user_pages/index.vue')),
|
||||
'Profile.vue': defineAsyncComponent(() => import('@/views/user_pages/Profile.vue')),
|
||||
'Documents.vue': defineAsyncComponent(() => import('@/views/user_pages/Documents.vue')),
|
||||
'Messages.vue': defineAsyncComponent(() => import('@/views/user_pages/Messages.vue')),
|
||||
'SystemMonitor.vue': defineAsyncComponent(() => import('@/views/user_pages/SystemMonitor.vue')),
|
||||
'AiChat.vue': defineAsyncComponent(() => import('@/views/user_pages/AiChat.vue')),
|
||||
'YoloDetection.vue': defineAsyncComponent(() => import('@/views/user_pages/YoloDetection.vue')),
|
||||
'LiveDetection.vue': defineAsyncComponent(() => import('@/views/user_pages/LiveDetection.vue')),
|
||||
'DetectionHistory.vue': defineAsyncComponent(() => import('@/views/user_pages/DetectionHistory.vue')),
|
||||
'AlertCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/AlertCenter.vue')),
|
||||
'NoticeCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/NoticeCenter.vue')),
|
||||
'KnowledgeCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/KnowledgeCenter.vue')),
|
||||
}
|
||||
|
||||
export const userMenuItems: MenuItem[] = userMenuConfigs.map(config => ({
|
||||
key: config.key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
path: config.path,
|
||||
disabled: config.disabled,
|
||||
children: config.children?.map(child => ({ key: child.key, label: child.label, icon: child.icon, path: child.path, disabled: child.disabled }))
|
||||
}))
|
||||
|
||||
const componentMap: Record<string, () => Promise<any>> = {
|
||||
'index.vue': () => import('@/views/user_pages/index.vue'),
|
||||
'Profile.vue': () => import('@/views/user_pages/Profile.vue'),
|
||||
'Documents.vue': () => import('@/views/user_pages/Documents.vue'),
|
||||
'Messages.vue': () => import('@/views/user_pages/Messages.vue'),
|
||||
'SystemMonitor.vue': () => import('@/views/user_pages/SystemMonitor.vue'),
|
||||
'AiChat.vue': () => import('@/views/user_pages/AiChat.vue'),
|
||||
'YoloDetection.vue': () => import('@/views/user_pages/YoloDetection.vue'),
|
||||
'LiveDetection.vue': () => import('@/views/user_pages/LiveDetection.vue'),
|
||||
'DetectionHistory.vue': () => import('@/views/user_pages/DetectionHistory.vue'),
|
||||
'AlertCenter.vue': () => import('@/views/user_pages/AlertCenter.vue'),
|
||||
'NoticeCenter.vue': () => import('@/views/user_pages/NoticeCenter.vue'),
|
||||
'KnowledgeCenter.vue': () => import('@/views/user_pages/KnowledgeCenter.vue'),
|
||||
}
|
||||
|
||||
const baseRoutes: RouteRecordRaw[] = userMenuConfigs.map(config => {
|
||||
const route: RouteRecordRaw = {
|
||||
path: config.path,
|
||||
name: `User${config.key.charAt(0).toUpperCase() + config.key.slice(1)}`,
|
||||
component: componentMap[config.component] || (() => import('@/views/NotFound.vue')),
|
||||
meta: { title: config.meta?.title || config.label, requiresAuth: config.meta?.requiresAuth ?? true, ...config.meta }
|
||||
}
|
||||
if (config.children && config.children.length > 0) {
|
||||
route.children = config.children.map(child => ({
|
||||
path: child.path,
|
||||
name: `User${child.key.charAt(0).toUpperCase() + child.key.slice(1)}`,
|
||||
component: componentMap[child.component] || (() => import('@/views/NotFound.vue')),
|
||||
meta: { title: child.meta?.title || child.label, requiresAuth: child.meta?.requiresAuth ?? true, ...child.meta }
|
||||
}))
|
||||
}
|
||||
return route
|
||||
})
|
||||
|
||||
// 文章详情独立页面(不在菜单展示)
|
||||
const knowledgeDetailRoute: RouteRecordRaw = {
|
||||
path: '/user/knowledge/:id',
|
||||
name: 'UserKnowledgeDetail',
|
||||
component: () => import('@/views/user_pages/KnowledgeDetail.vue'),
|
||||
meta: { title: '文章详情', requiresAuth: true, hideInMenu: true }
|
||||
}
|
||||
|
||||
export const userRoutes: RouteRecordRaw[] = [...baseRoutes, knowledgeDetailRoute]
|
||||
|
||||
export function getMenuPath(menuKey: string): string {
|
||||
const findPath = (items: MenuItem[], key: string): string | null => {
|
||||
for (const item of items) {
|
||||
if (item.key === key && item.path) return item.path
|
||||
if (item.children) {
|
||||
const childPath = findPath(item.children, key)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findPath(userMenuItems, menuKey) || '/dashboard'
|
||||
}
|
||||
|
||||
export function getMenuBreadcrumb(menuKey: string): string[] {
|
||||
const findBreadcrumb = (items: MenuItem[], key: string, path: string[] = []): string[] | null => {
|
||||
for (const item of items) {
|
||||
const currentPath = [...path, item.label]
|
||||
if (item.key === menuKey) return currentPath
|
||||
if (item.children) {
|
||||
const childPath = findBreadcrumb(item.children, key, currentPath)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findBreadcrumb(userMenuItems, menuKey) || ['仪表盘']
|
||||
}
|
||||
|
||||
export const generateComponentMap = () => {
|
||||
const map: Record<string, any> = {}
|
||||
const processConfigs = (configs: UserMenuConfig[]) => {
|
||||
configs.forEach(config => {
|
||||
if (explicitComponentMap[config.component]) {
|
||||
map[config.key] = explicitComponentMap[config.component]
|
||||
} else {
|
||||
map[config.key] = defineAsyncComponent(() => import('@/views/NotFound.vue'))
|
||||
}
|
||||
if (config.children) processConfigs(config.children)
|
||||
})
|
||||
}
|
||||
processConfigs(userMenuConfigs)
|
||||
return map
|
||||
}
|
||||
|
||||
export const userComponentMap = generateComponentMap()
|
||||
|
||||
export const getFilteredUserMenuItems = (userRoles: string[], userPermissions: string[]): MenuItem[] => {
|
||||
return userMenuConfigs
|
||||
.filter(config => {
|
||||
// 隐藏菜单中不显示的项(如个人信息,只在用户下拉菜单中显示)
|
||||
if (config.meta?.hideInMenu) return false
|
||||
if (!config.meta?.roles || config.meta.roles.length === 0) return true
|
||||
return config.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
})
|
||||
.map(config => ({
|
||||
key: config.key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
path: config.path,
|
||||
disabled: config.disabled,
|
||||
children: config.children?.filter(child => {
|
||||
if (!child.meta?.roles || child.meta.roles.length === 0) return true
|
||||
return child.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
}).map(child => ({ key: child.key, label: child.label, icon: child.icon, path: child.path, disabled: child.disabled }))
|
||||
}))
|
||||
}
|
||||
|
||||
export const hasUserMenuPermission = (menuKey: string, userRoles: string[]): boolean => {
|
||||
const menuConfig = userMenuConfigs.find(config => config.key === menuKey)
|
||||
if (!menuConfig) return false
|
||||
if (!menuConfig.meta?.roles || menuConfig.meta.roles.length === 0) return true
|
||||
return menuConfig.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
// 主题类型
|
||||
export type Theme = 'light' | 'dark' | 'auto'
|
||||
|
||||
// 语言类型
|
||||
export type Language = 'zh-CN' | 'en-US'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// 状态
|
||||
const theme = ref<Theme>('light')
|
||||
const language = ref<Language>('zh-CN')
|
||||
const collapsed = ref<boolean>(false)
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// 计算属性
|
||||
const isDark = computed(() => {
|
||||
if (theme.value === 'auto') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
return theme.value === 'dark'
|
||||
})
|
||||
|
||||
const currentLanguage = computed(() => language.value)
|
||||
|
||||
// 方法
|
||||
const setTheme = (newTheme: Theme) => {
|
||||
theme.value = newTheme
|
||||
localStorage.setItem('theme', newTheme)
|
||||
|
||||
// 应用主题到HTML
|
||||
const html = document.documentElement
|
||||
if (newTheme === 'dark' || (newTheme === 'auto' && isDark.value)) {
|
||||
html.classList.add('dark')
|
||||
} else {
|
||||
html.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
|
||||
const setLanguage = (newLanguage: Language) => {
|
||||
language.value = newLanguage
|
||||
localStorage.setItem('language', newLanguage)
|
||||
|
||||
// 设置i18n语言
|
||||
i18n.global.locale.value = newLanguage
|
||||
}
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
const setLoading = (state: boolean) => {
|
||||
loading.value = state
|
||||
}
|
||||
|
||||
const initAppSettings = () => {
|
||||
// 从本地存储恢复设置
|
||||
const savedTheme = localStorage.getItem('theme') as Theme
|
||||
const savedLanguage = localStorage.getItem('language') as Language
|
||||
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme)
|
||||
}
|
||||
|
||||
if (savedLanguage) {
|
||||
setLanguage(savedLanguage)
|
||||
} else {
|
||||
// 根据浏览器语言自动设置
|
||||
const browserLang = navigator.language
|
||||
if (browserLang.startsWith('zh')) {
|
||||
setLanguage('zh-CN')
|
||||
} else {
|
||||
setLanguage('en-US')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
theme,
|
||||
language,
|
||||
collapsed,
|
||||
loading,
|
||||
|
||||
// 计算属性
|
||||
isDark,
|
||||
currentLanguage,
|
||||
|
||||
// 方法
|
||||
setTheme,
|
||||
setLanguage,
|
||||
toggleCollapsed,
|
||||
setLoading,
|
||||
initAppSettings,
|
||||
}
|
||||
})
|
||||
@@ -1,101 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// 主题配置接口
|
||||
export interface ThemeConfig {
|
||||
// 导航栏
|
||||
headerBg: string
|
||||
headerText: string
|
||||
headerBorder: string
|
||||
|
||||
// 背景
|
||||
pageBg: string
|
||||
contentBg: string
|
||||
|
||||
// 组件背景
|
||||
cardBg: string
|
||||
cardBorder: string
|
||||
|
||||
// 主色调
|
||||
primaryColor: string
|
||||
textPrimary: string
|
||||
textSecondary: string
|
||||
}
|
||||
|
||||
// 默认主题
|
||||
const defaultTheme: ThemeConfig = {
|
||||
headerBg: '#ffffff',
|
||||
headerText: '#111827',
|
||||
headerBorder: '#e5e7eb',
|
||||
pageBg: '#ffffff',
|
||||
contentBg: '#ffffff',
|
||||
cardBg: '#ffffff',
|
||||
cardBorder: '#e5e7eb',
|
||||
primaryColor: '#2563eb',
|
||||
textPrimary: '#111827',
|
||||
textSecondary: '#6b7280',
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const theme = ref<ThemeConfig>({ ...defaultTheme })
|
||||
|
||||
// 从 localStorage 加载主题
|
||||
const loadTheme = () => {
|
||||
const savedTheme = localStorage.getItem('customTheme')
|
||||
if (savedTheme) {
|
||||
try {
|
||||
theme.value = { ...defaultTheme, ...JSON.parse(savedTheme) }
|
||||
applyTheme(theme.value)
|
||||
} catch (e) {
|
||||
console.error('Failed to load theme:', e)
|
||||
}
|
||||
} else {
|
||||
applyTheme(theme.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
const applyTheme = (config: ThemeConfig) => {
|
||||
const root = document.documentElement
|
||||
|
||||
// 设置 CSS 变量
|
||||
root.style.setProperty('--theme-header-bg', config.headerBg)
|
||||
root.style.setProperty('--theme-header-text', config.headerText)
|
||||
root.style.setProperty('--theme-header-border', config.headerBorder)
|
||||
root.style.setProperty('--theme-page-bg', config.pageBg)
|
||||
root.style.setProperty('--theme-content-bg', config.contentBg)
|
||||
root.style.setProperty('--theme-card-bg', config.cardBg)
|
||||
root.style.setProperty('--theme-card-border', config.cardBorder)
|
||||
root.style.setProperty('--theme-primary', config.primaryColor)
|
||||
root.style.setProperty('--theme-text-primary', config.textPrimary)
|
||||
root.style.setProperty('--theme-text-secondary', config.textSecondary)
|
||||
}
|
||||
|
||||
// 更新主题
|
||||
const updateTheme = (newTheme: Partial<ThemeConfig>) => {
|
||||
theme.value = { ...theme.value, ...newTheme }
|
||||
applyTheme(theme.value)
|
||||
localStorage.setItem('customTheme', JSON.stringify(theme.value))
|
||||
}
|
||||
|
||||
// 重置主题
|
||||
const resetTheme = () => {
|
||||
theme.value = { ...defaultTheme }
|
||||
applyTheme(theme.value)
|
||||
localStorage.removeItem('customTheme')
|
||||
}
|
||||
|
||||
// 监听主题变化,自动应用
|
||||
watch(theme, (newTheme) => {
|
||||
applyTheme(newTheme)
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
theme,
|
||||
loadTheme,
|
||||
updateTheme,
|
||||
resetTheme,
|
||||
applyTheme,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { request } from '@/utils/hertz_request'
|
||||
import { changePassword } from '@/api/password'
|
||||
import type { ChangePasswordParams } from '@/api/password'
|
||||
import { roleApi } from '@/api/role'
|
||||
import { initializeMenuMapping } from '@/utils/menu_mapping'
|
||||
import { logoutUser } from '@/api/auth'
|
||||
|
||||
// 用户信息接口
|
||||
interface UserInfo {
|
||||
user_id: number
|
||||
username: string
|
||||
email: string
|
||||
phone?: string
|
||||
real_name?: string
|
||||
avatar?: string
|
||||
roles: Array<{
|
||||
role_id: number
|
||||
role_name: string
|
||||
role_code: string
|
||||
}>
|
||||
permissions: string[]
|
||||
menu_permissions?: number[] // 用户拥有的菜单权限ID列表
|
||||
}
|
||||
|
||||
// 登录参数接口
|
||||
interface LoginParams {
|
||||
username: string
|
||||
password: string
|
||||
remember?: boolean
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 状态
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const token = ref<string>('')
|
||||
const isLoggedIn = ref<boolean>(false)
|
||||
const loading = ref<boolean>(false)
|
||||
const userMenuPermissions = ref<number[]>([]) // 用户菜单权限ID列表
|
||||
|
||||
// 计算属性
|
||||
const hasPermission = computed(() => (permission: string) => {
|
||||
return userInfo.value?.permissions?.includes(permission) || false
|
||||
})
|
||||
|
||||
const isAdmin = computed(() => {
|
||||
const userRole = userInfo.value?.roles?.[0]?.role_code
|
||||
return userRole === 'admin' || userRole === 'system_admin' || userRole === 'super_admin'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const login = async (params: LoginParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await request.post<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
user_info: UserInfo
|
||||
}>('/api/auth/login/', params)
|
||||
|
||||
token.value = response.access_token
|
||||
userInfo.value = response.user_info
|
||||
isLoggedIn.value = true
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('token', response.access_token)
|
||||
if (params.remember) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.user_info))
|
||||
}
|
||||
|
||||
// 获取用户菜单权限
|
||||
await fetchUserMenuPermissions()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 调用封装好的退出登录接口
|
||||
await logoutUser()
|
||||
|
||||
// 清除状态
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
isLoggedIn.value = false
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
// 即使请求失败也要清除本地状态
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
isLoggedIn.value = false
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateUserInfo = async (info: Partial<UserInfo>) => {
|
||||
try {
|
||||
const response = await request.put<UserInfo>('/user/profile', info)
|
||||
|
||||
userInfo.value = { ...userInfo.value, ...response }
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const checkAuth = async () => {
|
||||
console.log('🔍 检查用户认证状态...')
|
||||
|
||||
const savedToken = localStorage.getItem('token')
|
||||
const savedUserInfo = localStorage.getItem('userInfo')
|
||||
|
||||
console.log('💾 localStorage中的token:', savedToken ? '存在' : '不存在')
|
||||
console.log('💾 localStorage中的userInfo:', savedUserInfo ? '存在' : '不存在')
|
||||
|
||||
if (savedToken && savedUserInfo) {
|
||||
try {
|
||||
const parsedUserInfo = JSON.parse(savedUserInfo)
|
||||
token.value = savedToken
|
||||
userInfo.value = parsedUserInfo
|
||||
isLoggedIn.value = true
|
||||
|
||||
console.log('✅ 用户状态恢复成功')
|
||||
console.log('👤 恢复的用户信息:', parsedUserInfo)
|
||||
console.log('🔐 登录状态:', isLoggedIn.value)
|
||||
|
||||
// 获取用户菜单权限
|
||||
await fetchUserMenuPermissions()
|
||||
} catch (error) {
|
||||
console.error('❌ 解析用户信息失败:', error)
|
||||
clearAuth()
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 没有找到保存的认证信息')
|
||||
}
|
||||
}
|
||||
|
||||
const clearAuth = () => {
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
isLoggedIn.value = false
|
||||
userMenuPermissions.value = []
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
|
||||
const updatePassword = async (params: ChangePasswordParams) => {
|
||||
try {
|
||||
await changePassword(params)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户菜单权限
|
||||
const fetchUserMenuPermissions = async () => {
|
||||
if (!userInfo.value?.roles?.length) {
|
||||
userMenuPermissions.value = []
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户所有角色的菜单权限
|
||||
const allMenuPermissions = new Set<number>()
|
||||
|
||||
for (const role of userInfo.value.roles) {
|
||||
try {
|
||||
const response = await roleApi.getRolePermissions(role.role_id)
|
||||
if (response.success) {
|
||||
const menuIds = response.data.list || response.data
|
||||
if (Array.isArray(menuIds)) {
|
||||
menuIds.forEach((menuId: any) => {
|
||||
const id = typeof menuId === 'number' ? menuId : Number(menuId)
|
||||
if (!isNaN(id)) {
|
||||
allMenuPermissions.add(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取角色 ${role.role_name} 的菜单权限失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const permissions = Array.from(allMenuPermissions)
|
||||
userMenuPermissions.value = permissions
|
||||
|
||||
// 同时更新用户信息中的菜单权限
|
||||
if (userInfo.value) {
|
||||
userInfo.value.menu_permissions = permissions
|
||||
}
|
||||
|
||||
// 初始化菜单映射关系
|
||||
await initializeMenuMapping()
|
||||
|
||||
return permissions
|
||||
} catch (error) {
|
||||
console.error('获取用户菜单权限失败:', error)
|
||||
userMenuPermissions.value = []
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
userInfo,
|
||||
token,
|
||||
isLoggedIn,
|
||||
loading,
|
||||
userMenuPermissions,
|
||||
|
||||
// 计算属性
|
||||
hasPermission,
|
||||
isAdmin,
|
||||
|
||||
// 方法
|
||||
login,
|
||||
logout,
|
||||
updateUserInfo,
|
||||
checkAuth,
|
||||
clearAuth,
|
||||
updatePassword,
|
||||
fetchUserMenuPermissions,
|
||||
}
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
// 全局样式入口文件
|
||||
@use 'variables' as *;
|
||||
@use 'sass:color';
|
||||
|
||||
// 全局样式
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background: #ffffff;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 按钮样式
|
||||
.btn {
|
||||
@include transition(all);
|
||||
padding: $spacing-3 $spacing-6;
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: $radius-md;
|
||||
background: $bg-primary;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: $font-size-sm;
|
||||
line-height: 1.5;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
@include button-style($primary-color, white);
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: $bg-primary;
|
||||
color: $gray-700;
|
||||
border-color: $gray-300;
|
||||
|
||||
&:hover {
|
||||
background: $gray-50;
|
||||
border-color: $primary-color;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-success {
|
||||
@include button-style($success-color, white);
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
@include button-style($error-color, white);
|
||||
}
|
||||
|
||||
&.btn-warning {
|
||||
@include button-style($warning-color, white);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.card {
|
||||
@include card-style;
|
||||
padding: $spacing-6;
|
||||
margin-bottom: $spacing-4;
|
||||
}
|
||||
|
||||
// 表单样式
|
||||
.form-item {
|
||||
margin-bottom: $spacing-4;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: $spacing-2;
|
||||
font-weight: 500;
|
||||
color: $gray-700;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: $spacing-3;
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-sm;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 布局辅助类
|
||||
.flex-center {
|
||||
@include flex-center;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
@include text-ellipsis;
|
||||
}
|
||||
|
||||
// 间距辅助类
|
||||
.m-0 { margin: $spacing-0; }
|
||||
.m-1 { margin: $spacing-1; }
|
||||
.m-2 { margin: $spacing-2; }
|
||||
.m-3 { margin: $spacing-3; }
|
||||
.m-4 { margin: $spacing-4; }
|
||||
.m-5 { margin: $spacing-5; }
|
||||
.m-6 { margin: $spacing-6; }
|
||||
.m-8 { margin: $spacing-8; }
|
||||
|
||||
.p-0 { padding: $spacing-0; }
|
||||
.p-1 { padding: $spacing-1; }
|
||||
.p-2 { padding: $spacing-2; }
|
||||
.p-3 { padding: $spacing-3; }
|
||||
.p-4 { padding: $spacing-4; }
|
||||
.p-5 { padding: $spacing-5; }
|
||||
.p-6 { padding: $spacing-6; }
|
||||
.p-8 { padding: $spacing-8; }
|
||||
|
||||
// ==================== 全局弹窗美化样式 - 苹果风格 ====================
|
||||
// 弹窗遮罩层
|
||||
.ant-modal-mask {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
// 弹窗容器
|
||||
.ant-modal-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 统一按钮主题 - 苹果风格
|
||||
.ant-btn {
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
padding: 0 20px;
|
||||
height: 40px;
|
||||
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
border-width: 0.5px;
|
||||
|
||||
&.ant-btn-default {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
color: #1d1d1f;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-color: rgba(0, 0, 0, 0.16);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
&:active { transform: translateY(0); }
|
||||
}
|
||||
|
||||
&.ant-btn-dangerous:not(.ant-btn-link) {
|
||||
color: #ef4444;
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-link {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.ant-btn-sm {
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗内容 - 苹果风格
|
||||
.ant-modal {
|
||||
top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
.ant-modal-content {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 0.5px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
animation: modalSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
// 弹窗头部
|
||||
.ant-modal-header {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 24px 28px;
|
||||
border-radius: 20px 20px 0 0;
|
||||
|
||||
.ant-modal-title {
|
||||
font-weight: 600;
|
||||
color: #1d1d1f;
|
||||
font-size: 20px;
|
||||
letter-spacing: -0.3px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
top: 24px;
|
||||
right: 28px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
color: #86868b;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover { color: #1d1d1f; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗主体
|
||||
.ant-modal-body {
|
||||
padding: 28px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #1d1d1f;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// 弹窗底部
|
||||
.ant-modal-footer {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
border-top: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 20px 28px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
|
||||
.ant-btn {
|
||||
border-radius: 10px;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
&:not(.ant-btn-primary) {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #1d1d1f;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-color: rgba(0, 0, 0, 0.16);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
&:active { transform: translateY(0); }
|
||||
}
|
||||
|
||||
&.ant-btn-dangerous {
|
||||
color: #ef4444;
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单元素美化
|
||||
.ant-form-item-label > label {
|
||||
font-weight: 500;
|
||||
color: #1d1d1f;
|
||||
font-size: 14px;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.ant-input,
|
||||
.ant-select-selector,
|
||||
.ant-input-number,
|
||||
.ant-picker,
|
||||
.ant-textarea,
|
||||
.ant-tree-select-selector {
|
||||
border-radius: 10px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
font-size: 14px;
|
||||
|
||||
&:hover { border-color: #3b82f6; background: rgba(255, 255, 255, 1); }
|
||||
&:focus,
|
||||
&.ant-input-focused,
|
||||
&.ant-select-focused .ant-select-selector,
|
||||
&.ant-picker-focused { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); background: rgba(255, 255, 255, 1); }
|
||||
}
|
||||
|
||||
.ant-input-number { width: 100%; }
|
||||
|
||||
.ant-radio-group {
|
||||
.ant-radio-button-wrapper {
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
&:hover { border-color: #3b82f6; }
|
||||
&.ant-radio-button-wrapper-checked { background: #3b82f6; border-color: #3b82f6; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
|
||||
}
|
||||
}
|
||||
|
||||
.ant-switch { background: rgba(0, 0, 0, 0.25); &.ant-switch-checked { background: #10b981; } }
|
||||
|
||||
// 表格在弹窗中的样式
|
||||
.ant-table {
|
||||
background: transparent;
|
||||
.ant-table-thead > tr > th {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
font-weight: 600;
|
||||
color: #1d1d1f;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.ant-table-tbody > tr {
|
||||
transition: all 0.2s ease;
|
||||
&:hover > td { background: rgba(0, 0, 0, 0.02); }
|
||||
> td { padding: 16px; border-bottom: 0.5px solid rgba(0, 0, 0, 0.06); color: #1d1d1f; }
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式
|
||||
.ant-tag { border-radius: 6px; font-weight: 500; padding: 2px 10px; border: 0.5px solid currentColor; opacity: 0.8; }
|
||||
|
||||
// 描述列表样式
|
||||
.ant-descriptions {
|
||||
.ant-descriptions-item-label { font-weight: 500; color: #1d1d1f; background: rgba(0, 0, 0, 0.02); }
|
||||
.ant-descriptions-item-content { color: #86868b; }
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗动画
|
||||
@keyframes modalSlideIn {
|
||||
from { opacity: 0; transform: scale(0.95) translateY(-20px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
// 响应式优化
|
||||
@media (max-width: 768px) {
|
||||
.ant-modal {
|
||||
.ant-modal-content { border-radius: 16px; }
|
||||
.ant-modal-header { padding: 20px 20px; border-radius: 16px 16px 0 0; .ant-modal-title { font-size: 18px; } }
|
||||
.ant-modal-body { padding: 20px; }
|
||||
.ant-modal-footer { padding: 16px 20px; border-radius: 0 0 16px 16px; }
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// 全局变量文件 - 简约现代风格
|
||||
|
||||
// 颜色系统
|
||||
$primary-color: #2563eb;
|
||||
$primary-light: #3b82f6;
|
||||
$primary-dark: #1d4ed8;
|
||||
$success-color: #10b981;
|
||||
$warning-color: #f59e0b;
|
||||
$error-color: #ef4444;
|
||||
$info-color: #06b6d4;
|
||||
|
||||
// 中性色系统
|
||||
$gray-50: #f9fafb;
|
||||
$gray-100: #f3f4f6;
|
||||
$gray-200: #e5e7eb;
|
||||
$gray-300: #d1d5db;
|
||||
$gray-400: #9ca3af;
|
||||
$gray-500: #6b7280;
|
||||
$gray-600: #4b5563;
|
||||
$gray-700: #374151;
|
||||
$gray-800: #1f2937;
|
||||
$gray-900: #111827;
|
||||
|
||||
// 背景色
|
||||
$bg-primary: #ffffff;
|
||||
$bg-secondary: #f9fafb;
|
||||
$bg-tertiary: #f3f4f6;
|
||||
|
||||
// 字体大小
|
||||
$font-size-xs: 12px;
|
||||
$font-size-sm: 14px;
|
||||
$font-size-base: 16px;
|
||||
$font-size-lg: 18px;
|
||||
$font-size-xl: 20px;
|
||||
$font-size-2xl: 24px;
|
||||
$font-size-3xl: 30px;
|
||||
$font-size-4xl: 36px;
|
||||
|
||||
// 间距系统 - 4px基础单位
|
||||
$spacing-0: 0;
|
||||
$spacing-1: 4px;
|
||||
$spacing-2: 8px;
|
||||
$spacing-3: 12px;
|
||||
$spacing-4: 16px;
|
||||
$spacing-5: 20px;
|
||||
$spacing-6: 24px;
|
||||
$spacing-8: 32px;
|
||||
$spacing-10: 40px;
|
||||
$spacing-12: 48px;
|
||||
$spacing-16: 64px;
|
||||
$spacing-20: 80px;
|
||||
|
||||
// 圆角系统
|
||||
$radius-none: 0;
|
||||
$radius-sm: 4px;
|
||||
$radius-md: 6px;
|
||||
$radius-lg: 8px;
|
||||
$radius-xl: 12px;
|
||||
$radius-2xl: 16px;
|
||||
$radius-full: 9999px;
|
||||
|
||||
// 阴影系统
|
||||
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
// 过渡时间
|
||||
$transition-fast: 0.15s;
|
||||
$transition-normal: 0.2s;
|
||||
$transition-slow: 0.3s;
|
||||
|
||||
// 混合器
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@mixin text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin box-shadow($shadow: $shadow-md) {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
|
||||
@mixin transition($property: all, $duration: $transition-normal) {
|
||||
transition: #{$property} #{$duration} ease;
|
||||
}
|
||||
|
||||
@mixin card-style {
|
||||
background: $bg-primary;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
border: 1px solid $gray-200;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-style($bg-color: $primary-color, $text-color: white) {
|
||||
background: $bg-color;
|
||||
color: $text-color;
|
||||
border: 1px solid $bg-color;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-3 $spacing-6;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: darken($bg-color, 8%);
|
||||
border-color: darken($bg-color, 8%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: darken($bg-color, 12%);
|
||||
}
|
||||
}
|
||||
13
hertz_server_diango_ui/src/types/env.d.ts
vendored
13
hertz_server_diango_ui/src/types/env.d.ts
vendored
@@ -1,13 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string
|
||||
readonly VITE_APP_TITLE: string
|
||||
readonly VITE_APP_VERSION: string
|
||||
readonly VITE_DEV_SERVER_HOST: string
|
||||
readonly VITE_DEV_SERVER_PORT: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
success?: boolean
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
// 分页请求参数
|
||||
export interface PageParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
sortBy?: string
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
// 分页响应
|
||||
export interface PageResponse<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
// 用户相关类型
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
avatar?: string
|
||||
role: string
|
||||
permissions: string[]
|
||||
status: 'active' | 'inactive' | 'banned'
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
export interface LoginParams {
|
||||
username: string
|
||||
password: string
|
||||
remember?: boolean
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: User
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
// 菜单相关类型
|
||||
export interface MenuItem {
|
||||
id: number
|
||||
name: string
|
||||
path: string
|
||||
icon?: string
|
||||
children?: MenuItem[]
|
||||
permission?: string
|
||||
hidden?: boolean
|
||||
meta?: {
|
||||
title: string
|
||||
requiresAuth?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// 表格相关类型
|
||||
export interface TableColumn<T = any> {
|
||||
key: string
|
||||
title: string
|
||||
width?: number
|
||||
fixed?: 'left' | 'right'
|
||||
sortable?: boolean
|
||||
render?: (record: T, index: number) => any
|
||||
}
|
||||
|
||||
export interface TableProps<T = any> {
|
||||
data: T[]
|
||||
columns: TableColumn<T>[]
|
||||
loading?: boolean
|
||||
pagination?: {
|
||||
current: number
|
||||
pageSize: number
|
||||
total: number
|
||||
showSizeChanger?: boolean
|
||||
showQuickJumper?: boolean
|
||||
}
|
||||
rowSelection?: {
|
||||
selectedRowKeys: (string | number)[]
|
||||
onChange: (selectedRowKeys: (string | number)[], selectedRows: T[]) => void
|
||||
}
|
||||
}
|
||||
|
||||
// 表单相关类型
|
||||
export interface FormField {
|
||||
name: string
|
||||
label: string
|
||||
type: 'input' | 'select' | 'textarea' | 'date' | 'switch' | 'radio' | 'checkbox'
|
||||
required?: boolean
|
||||
placeholder?: string
|
||||
options?: { label: string; value: any }[]
|
||||
rules?: any[]
|
||||
}
|
||||
|
||||
export interface FormProps {
|
||||
fields: FormField[]
|
||||
initialValues?: Record<string, any>
|
||||
onSubmit: (values: Record<string, any>) => Promise<void>
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
// 弹窗相关类型
|
||||
export interface ModalProps {
|
||||
title: string
|
||||
visible: boolean
|
||||
onCancel: () => void
|
||||
onOk?: () => void
|
||||
width?: number
|
||||
children: any
|
||||
}
|
||||
|
||||
// 消息相关类型
|
||||
export type MessageType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export interface MessageConfig {
|
||||
type: MessageType
|
||||
content: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
// 主题相关类型
|
||||
export type Theme = 'light' | 'dark' | 'auto'
|
||||
|
||||
// 语言相关类型
|
||||
export type Language = 'zh-CN' | 'en-US'
|
||||
|
||||
// 路由相关类型
|
||||
export interface RouteMeta {
|
||||
title?: string
|
||||
requiresAuth?: boolean
|
||||
permission?: string
|
||||
hidden?: boolean
|
||||
icon?: string
|
||||
}
|
||||
|
||||
// 组件属性类型
|
||||
export interface ComponentProps {
|
||||
className?: string
|
||||
style?: Record<string, any>
|
||||
children?: any
|
||||
}
|
||||
|
||||
// 工具函数类型
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
||||
}
|
||||
|
||||
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
||||
|
||||
// API 相关类型
|
||||
export interface RequestConfig {
|
||||
showLoading?: boolean
|
||||
showError?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
// 文件相关类型
|
||||
export interface FileInfo {
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
url?: string
|
||||
lastModified: number
|
||||
}
|
||||
|
||||
export interface UploadProps {
|
||||
accept?: string
|
||||
multiple?: boolean
|
||||
maxSize?: number
|
||||
onUpload: (files: File[]) => Promise<void>
|
||||
onRemove?: (file: FileInfo) => void
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { generateCaptcha, refreshCaptcha, type CaptchaResponse, type CaptchaRefreshResponse } from '@/api/captcha'
|
||||
import { ref, type Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* 验证码组合式函数
|
||||
*/
|
||||
export function useCaptcha() {
|
||||
// 验证码数据
|
||||
const captchaData: Ref<CaptchaResponse | null> = ref(null)
|
||||
|
||||
// 加载状态
|
||||
const captchaLoading: Ref<boolean> = ref(false)
|
||||
|
||||
// 错误信息
|
||||
const captchaError: Ref<string | null> = ref(null)
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
const handleGenerateCaptcha = async (): Promise<void> => {
|
||||
try {
|
||||
captchaLoading.value = true
|
||||
captchaError.value = null
|
||||
|
||||
const response = await generateCaptcha()
|
||||
captchaData.value = response
|
||||
} catch (error) {
|
||||
console.error('生成验证码失败:', error)
|
||||
captchaError.value = error instanceof Error ? error.message : '生成验证码失败'
|
||||
} finally {
|
||||
captchaLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新验证码
|
||||
*/
|
||||
const handleRefreshCaptcha = async (): Promise<void> => {
|
||||
try {
|
||||
captchaLoading.value = true
|
||||
captchaError.value = null
|
||||
|
||||
// 检查是否有当前验证码ID
|
||||
if (!captchaData.value?.captcha_id) {
|
||||
console.warn('没有当前验证码ID,将生成新的验证码')
|
||||
await handleGenerateCaptcha()
|
||||
return
|
||||
}
|
||||
|
||||
const response = await refreshCaptcha(captchaData.value.captcha_id)
|
||||
captchaData.value = response
|
||||
} catch (error) {
|
||||
console.error('刷新验证码失败:', error)
|
||||
captchaError.value = error instanceof Error ? error.message : '刷新验证码失败'
|
||||
} finally {
|
||||
captchaLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
captchaData,
|
||||
captchaLoading,
|
||||
captchaError,
|
||||
generateCaptcha: handleGenerateCaptcha,
|
||||
refreshCaptcha: handleRefreshCaptcha
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型
|
||||
export type { CaptchaResponse, CaptchaRefreshResponse }
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* 环境变量检查工具
|
||||
* 用于在开发环境中检查环境变量配置是否正确
|
||||
*/
|
||||
|
||||
// 检查环境变量配置
|
||||
export const checkEnvironmentVariables = () => {
|
||||
console.log('🔧 环境变量检查')
|
||||
|
||||
// 在Vite中,环境变量可能通过define选项直接定义
|
||||
// 或者通过import.meta.env读取
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://hertzServer:8000/api'
|
||||
const appTitle = import.meta.env.VITE_APP_TITLE || 'Hertz Admin'
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
|
||||
|
||||
// 检查必需的环境变量
|
||||
const requiredVars = [
|
||||
{ key: 'VITE_API_BASE_URL', value: apiBaseUrl },
|
||||
{ key: 'VITE_APP_TITLE', value: appTitle },
|
||||
{ key: 'VITE_APP_VERSION', value: appVersion },
|
||||
]
|
||||
|
||||
requiredVars.forEach(({ key, value }) => {
|
||||
if (value) {
|
||||
console.log(`✅ ${key}: ${value}`)
|
||||
} else {
|
||||
console.warn(`❌ ${key}: 未设置`)
|
||||
}
|
||||
})
|
||||
|
||||
// 检查可选的环境变量
|
||||
const devServerHost = import.meta.env.VITE_DEV_SERVER_HOST || 'localhost'
|
||||
const devServerPort = import.meta.env.VITE_DEV_SERVER_PORT || '3000'
|
||||
|
||||
const optionalVars = [
|
||||
{ key: 'VITE_DEV_SERVER_HOST', value: devServerHost },
|
||||
{ key: 'VITE_DEV_SERVER_PORT', value: devServerPort },
|
||||
]
|
||||
|
||||
optionalVars.forEach(({ key, value }) => {
|
||||
if (value) {
|
||||
console.log(`ℹ️ ${key}: ${value}`)
|
||||
} else {
|
||||
console.log(`➖ ${key}: 未设置(使用默认值)`)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('🎉 环境变量检查完成')
|
||||
}
|
||||
|
||||
// 验证环境变量是否有效
|
||||
export const validateEnvironment = () => {
|
||||
// 检查API基础地址
|
||||
if (!import.meta.env.VITE_API_BASE_URL) {
|
||||
console.warn('⚠️ VITE_API_BASE_URL 未设置,将使用默认值')
|
||||
}
|
||||
|
||||
// 检查应用配置
|
||||
if (!import.meta.env.VITE_APP_TITLE) {
|
||||
console.warn('⚠️ VITE_APP_TITLE 未设置,将使用默认值')
|
||||
}
|
||||
|
||||
if (!import.meta.env.VITE_APP_VERSION) {
|
||||
console.warn('⚠️ VITE_APP_VERSION 未设置,将使用默认值')
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
warnings: []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取API基础地址
|
||||
export const getApiBaseUrl = (): string => {
|
||||
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api'
|
||||
}
|
||||
|
||||
// 获取应用配置
|
||||
export const getAppConfig = () => {
|
||||
return {
|
||||
title: import.meta.env.VITE_APP_TITLE || 'Hertz Admin',
|
||||
version: import.meta.env.VITE_APP_VERSION || '1.0.0',
|
||||
apiBaseUrl: getApiBaseUrl(),
|
||||
devServerHost: import.meta.env.VITE_DEV_SERVER_HOST || 'localhost',
|
||||
devServerPort: import.meta.env.VITE_DEV_SERVER_PORT || '3000',
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 错误类型枚举
|
||||
export enum ErrorType {
|
||||
// 网络错误
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
TIMEOUT = 'TIMEOUT',
|
||||
|
||||
// 认证错误
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
|
||||
TOKEN_INVALID = 'TOKEN_INVALID',
|
||||
|
||||
// 权限错误
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
ACCESS_DENIED = 'ACCESS_DENIED',
|
||||
|
||||
// 业务错误
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
BUSINESS_ERROR = 'BUSINESS_ERROR',
|
||||
|
||||
// 系统错误
|
||||
SERVER_ERROR = 'SERVER_ERROR',
|
||||
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
||||
}
|
||||
|
||||
// 错误信息接口
|
||||
export interface ErrorInfo {
|
||||
code: number
|
||||
message: string
|
||||
type: ErrorType
|
||||
details?: any
|
||||
field?: string
|
||||
}
|
||||
|
||||
// 错误处理器类
|
||||
export class HertzErrorHandler {
|
||||
private static instance: HertzErrorHandler
|
||||
private i18n: any
|
||||
|
||||
constructor() {
|
||||
// 在组件中使用时需要传入i18n实例
|
||||
}
|
||||
|
||||
static getInstance(): HertzErrorHandler {
|
||||
if (!HertzErrorHandler.instance) {
|
||||
HertzErrorHandler.instance = new HertzErrorHandler()
|
||||
}
|
||||
return HertzErrorHandler.instance
|
||||
}
|
||||
|
||||
// 设置i18n实例
|
||||
setI18n(i18n: any) {
|
||||
this.i18n = i18n
|
||||
}
|
||||
|
||||
// 获取翻译文本
|
||||
private t(key: string, fallback?: string): string {
|
||||
if (this.i18n && this.i18n.t) {
|
||||
return this.i18n.t(key)
|
||||
}
|
||||
return fallback || key
|
||||
}
|
||||
|
||||
// 处理HTTP错误
|
||||
handleHttpError(error: any): void {
|
||||
const status = error?.response?.status
|
||||
const data = error?.response?.data
|
||||
|
||||
console.error('🚨 HTTP错误详情:', {
|
||||
status,
|
||||
data,
|
||||
url: error?.config?.url,
|
||||
method: error?.config?.method,
|
||||
requestData: error?.config?.data
|
||||
})
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
this.handleBadRequestError(data)
|
||||
break
|
||||
case 401:
|
||||
this.handleUnauthorizedError(data)
|
||||
break
|
||||
case 403:
|
||||
this.handleForbiddenError(data)
|
||||
break
|
||||
case 404:
|
||||
this.handleNotFoundError(data)
|
||||
break
|
||||
case 422:
|
||||
this.handleValidationError(data)
|
||||
break
|
||||
case 429:
|
||||
this.handleTooManyRequestsError(data)
|
||||
break
|
||||
case 500:
|
||||
this.handleServerError(data)
|
||||
break
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
this.handleServiceUnavailableError(data)
|
||||
break
|
||||
default:
|
||||
this.handleUnknownError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理400错误
|
||||
private handleBadRequestError(data: any): void {
|
||||
const message = data?.message || data?.detail || ''
|
||||
|
||||
// 检查是否是验证码相关错误
|
||||
if (this.isMessageContains(message, ['验证码', 'captcha', 'Captcha'])) {
|
||||
if (this.isMessageContains(message, ['过期', 'expired', 'expire'])) {
|
||||
this.showError(this.t('error.captchaExpired', '验证码已过期,请刷新后重新输入'))
|
||||
} else {
|
||||
this.showError(this.t('error.captchaError', '验证码错误,请重新输入(区分大小写)'))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是用户名或密码错误
|
||||
if (this.isMessageContains(message, ['用户名', 'username', '密码', 'password', '登录', 'login'])) {
|
||||
this.showError(this.t('error.loginFailed', '登录失败,请检查用户名和密码'))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是注册相关错误
|
||||
if (this.isMessageContains(message, ['用户名已存在', 'username exists', 'username already'])) {
|
||||
this.showError(this.t('error.usernameExists', '用户名已存在,请选择其他用户名'))
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isMessageContains(message, ['邮箱已注册', 'email exists', 'email already'])) {
|
||||
this.showError(this.t('error.emailExists', '邮箱已被注册,请使用其他邮箱'))
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isMessageContains(message, ['手机号已注册', 'phone exists', 'phone already'])) {
|
||||
this.showError(this.t('error.phoneExists', '手机号已被注册,请使用其他手机号'))
|
||||
return
|
||||
}
|
||||
|
||||
// 默认400错误处理
|
||||
this.showError(data?.message || this.t('error.invalidInput', '输入数据格式不正确'))
|
||||
}
|
||||
|
||||
// 处理401错误
|
||||
private handleUnauthorizedError(data: any): void {
|
||||
const message = data?.message || data?.detail || ''
|
||||
|
||||
if (this.isMessageContains(message, ['token', 'Token', '令牌', '过期', 'expired'])) {
|
||||
this.showError(this.t('error.tokenExpired', '登录已过期,请重新登录'))
|
||||
// 可以在这里添加自动跳转到登录页的逻辑
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login'
|
||||
}, 2000)
|
||||
} else if (this.isMessageContains(message, ['账户锁定', 'account locked', 'locked'])) {
|
||||
this.showError(this.t('error.accountLocked', '账户已被锁定,请联系管理员'))
|
||||
} else if (this.isMessageContains(message, ['账户禁用', 'account disabled', 'disabled'])) {
|
||||
this.showError(this.t('error.accountDisabled', '账户已被禁用,请联系管理员'))
|
||||
} else {
|
||||
this.showError(this.t('error.loginFailed', '登录失败,请检查用户名和密码'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理403错误
|
||||
private handleForbiddenError(data: any): void {
|
||||
const message = data?.message || data?.detail || ''
|
||||
|
||||
if (this.isMessageContains(message, ['权限不足', 'permission denied', 'access denied'])) {
|
||||
this.showError(this.t('error.permissionDenied', '权限不足,无法执行此操作'))
|
||||
} else {
|
||||
this.showError(this.t('error.accessDenied', '访问被拒绝,您没有执行此操作的权限'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理404错误
|
||||
private handleNotFoundError(data: any): void {
|
||||
const message = data?.message || data?.detail || ''
|
||||
|
||||
if (this.isMessageContains(message, ['用户', 'user'])) {
|
||||
this.showError(this.t('error.userNotFound', '用户不存在或已被删除'))
|
||||
} else if (this.isMessageContains(message, ['部门', 'department'])) {
|
||||
this.showError(this.t('error.departmentNotFound', '部门不存在或已被删除'))
|
||||
} else if (this.isMessageContains(message, ['角色', 'role'])) {
|
||||
this.showError(this.t('error.roleNotFound', '角色不存在或已被删除'))
|
||||
} else {
|
||||
this.showError(this.t('error.404', '页面未找到'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理422验证错误
|
||||
private handleValidationError(data: any): void {
|
||||
console.log('🔍 422验证错误详情:', data)
|
||||
|
||||
// 处理FastAPI风格的验证错误
|
||||
if (data?.detail && Array.isArray(data.detail)) {
|
||||
const errors = data.detail
|
||||
const errorMessages: string[] = []
|
||||
|
||||
errors.forEach((error: any) => {
|
||||
const field = error.loc?.[error.loc.length - 1] || 'unknown'
|
||||
const msg = error.msg || error.message || '验证失败'
|
||||
|
||||
// 根据字段和错误类型提供更具体的提示
|
||||
if (field === 'username') {
|
||||
if (msg.includes('required') || msg.includes('必填')) {
|
||||
errorMessages.push(this.t('error.usernameRequired', '请输入用户名'))
|
||||
} else if (msg.includes('length') || msg.includes('长度')) {
|
||||
errorMessages.push('用户名长度不符合要求')
|
||||
} else {
|
||||
errorMessages.push(`用户名: ${msg}`)
|
||||
}
|
||||
} else if (field === 'password') {
|
||||
if (msg.includes('required') || msg.includes('必填')) {
|
||||
errorMessages.push(this.t('error.passwordRequired', '请输入密码'))
|
||||
} else if (msg.includes('weak') || msg.includes('强度')) {
|
||||
errorMessages.push(this.t('error.passwordTooWeak', '密码强度不足,请包含大小写字母、数字和特殊字符'))
|
||||
} else {
|
||||
errorMessages.push(`密码: ${msg}`)
|
||||
}
|
||||
} else if (field === 'email') {
|
||||
if (msg.includes('format') || msg.includes('格式')) {
|
||||
errorMessages.push(this.t('error.emailFormatError', '邮箱格式不正确,请输入有效的邮箱地址'))
|
||||
} else {
|
||||
errorMessages.push(`邮箱: ${msg}`)
|
||||
}
|
||||
} else if (field === 'phone') {
|
||||
if (msg.includes('format') || msg.includes('格式')) {
|
||||
errorMessages.push(this.t('error.phoneFormatError', '手机号格式不正确,请输入11位手机号'))
|
||||
} else {
|
||||
errorMessages.push(`手机号: ${msg}`)
|
||||
}
|
||||
} else if (field === 'captcha' || field === 'captcha_code') {
|
||||
errorMessages.push(this.t('error.captchaError', '验证码错误,请重新输入(区分大小写)'))
|
||||
} else {
|
||||
errorMessages.push(`${field}: ${msg}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (errorMessages.length > 0) {
|
||||
this.showError(errorMessages.join(';'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他格式的验证错误
|
||||
if (data?.errors) {
|
||||
const errors = data.errors
|
||||
const errorMessages = []
|
||||
for (const field in errors) {
|
||||
if (errors[field] && Array.isArray(errors[field])) {
|
||||
errorMessages.push(`${field}: ${errors[field].join(', ')}`)
|
||||
} else if (errors[field]) {
|
||||
errorMessages.push(`${field}: ${errors[field]}`)
|
||||
}
|
||||
}
|
||||
if (errorMessages.length > 0) {
|
||||
this.showError(`验证失败: ${errorMessages.join('; ')}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 默认验证错误处理
|
||||
this.showError(data?.message || this.t('error.invalidInput', '输入数据格式不正确'))
|
||||
}
|
||||
|
||||
// 处理429错误(请求过多)
|
||||
private handleTooManyRequestsError(data: any): void {
|
||||
this.showError(this.t('error.loginAttemptsExceeded', '登录尝试次数过多,账户已被临时锁定'))
|
||||
}
|
||||
|
||||
// 处理500错误
|
||||
private handleServerError(data: any): void {
|
||||
this.showError(this.t('error.500', '服务器内部错误,请稍后重试'))
|
||||
}
|
||||
|
||||
// 处理服务不可用错误
|
||||
private handleServiceUnavailableError(data: any): void {
|
||||
this.showError(this.t('error.serviceUnavailable', '服务暂时不可用,请稍后重试'))
|
||||
}
|
||||
|
||||
// 处理网络错误
|
||||
handleNetworkError(error: any): void {
|
||||
if (error?.code === 'NETWORK_ERROR' || error?.message?.includes('Network Error')) {
|
||||
this.showError(this.t('error.networkError', '网络连接失败,请检查网络设置'))
|
||||
} else if (error?.code === 'ECONNABORTED' || error?.message?.includes('timeout')) {
|
||||
this.showError(this.t('error.timeout', '请求超时,请稍后重试'))
|
||||
} else {
|
||||
this.showError(this.t('error.networkError', '网络连接失败,请检查网络设置'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理未知错误
|
||||
private handleUnknownError(error: any): void {
|
||||
console.error('🚨 未知错误:', error)
|
||||
this.showError(this.t('error.operationFailed', '操作失败,请稍后重试'))
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
private showError(msg: string): void {
|
||||
message.error(msg)
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
showSuccess(msg: string): void {
|
||||
message.success(msg)
|
||||
}
|
||||
|
||||
// 显示警告消息
|
||||
showWarning(msg: string): void {
|
||||
message.warning(msg)
|
||||
}
|
||||
|
||||
// 检查消息是否包含指定关键词
|
||||
private isMessageContains(message: string, keywords: string[]): boolean {
|
||||
if (!message) return false
|
||||
const lowerMessage = message.toLowerCase()
|
||||
return keywords.some(keyword => lowerMessage.includes(keyword.toLowerCase()))
|
||||
}
|
||||
|
||||
// 处理业务操作成功
|
||||
handleSuccess(operation: string, customMessage?: string): void {
|
||||
if (customMessage) {
|
||||
this.showSuccess(customMessage)
|
||||
return
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'save':
|
||||
this.showSuccess(this.t('error.saveSuccess', '保存成功'))
|
||||
break
|
||||
case 'delete':
|
||||
this.showSuccess(this.t('error.deleteSuccess', '删除成功'))
|
||||
break
|
||||
case 'update':
|
||||
this.showSuccess(this.t('error.updateSuccess', '更新成功'))
|
||||
break
|
||||
case 'create':
|
||||
this.showSuccess('创建成功')
|
||||
break
|
||||
case 'login':
|
||||
this.showSuccess('登录成功')
|
||||
break
|
||||
case 'register':
|
||||
this.showSuccess('注册成功')
|
||||
break
|
||||
default:
|
||||
this.showSuccess('操作成功')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const errorHandler = HertzErrorHandler.getInstance()
|
||||
|
||||
// 导出便捷方法
|
||||
export const handleError = (error: any) => {
|
||||
if (error?.response) {
|
||||
errorHandler.handleHttpError(error)
|
||||
} else if (error?.code === 'NETWORK_ERROR' || error?.message?.includes('Network Error')) {
|
||||
errorHandler.handleNetworkError(error)
|
||||
} else {
|
||||
console.error('🚨 处理错误:', error)
|
||||
errorHandler.showError('操作失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
export const handleSuccess = (operation: string, customMessage?: string) => {
|
||||
errorHandler.handleSuccess(operation, customMessage)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/**
|
||||
* 权限管理工具类
|
||||
* 统一管理用户权限检查和菜单过滤逻辑
|
||||
*/
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/stores/hertz_user'
|
||||
import { UserRole } from '@/router/admin_menu'
|
||||
|
||||
// 权限检查接口
|
||||
export interface PermissionChecker {
|
||||
hasRole(role: string): boolean
|
||||
hasPermission(permission: string): boolean
|
||||
hasAnyRole(roles: string[]): boolean
|
||||
hasAnyPermission(permissions: string[]): boolean
|
||||
isAdmin(): boolean
|
||||
isLoggedIn(): boolean
|
||||
}
|
||||
|
||||
// 权限管理类
|
||||
export class PermissionManager implements PermissionChecker {
|
||||
// 延迟获取 Pinia store,避免在 Pinia 未初始化时调用
|
||||
private get userStore() {
|
||||
return useUserStore()
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定角色
|
||||
*/
|
||||
hasRole(role: string): boolean {
|
||||
const userRoles = this.userStore.userInfo?.roles?.map(r => r.role_code) || []
|
||||
return userRoles.includes(role)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定权限
|
||||
*/
|
||||
hasPermission(permission: string): boolean {
|
||||
const userPermissions = this.userStore.userInfo?.permissions || []
|
||||
return userPermissions.includes(permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有任意一个指定角色
|
||||
*/
|
||||
hasAnyRole(roles: string[]): boolean {
|
||||
const userRoles = this.userStore.userInfo?.roles?.map(r => r.role_code) || []
|
||||
return roles.some(role => userRoles.includes(role))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有任意一个指定权限
|
||||
*/
|
||||
hasAnyPermission(permissions: string[]): boolean {
|
||||
const userPermissions = this.userStore.userInfo?.permissions || []
|
||||
return permissions.some(permission => userPermissions.includes(permission))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否为管理员
|
||||
*/
|
||||
isAdmin(): boolean {
|
||||
const adminRoles = [UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN]
|
||||
return this.hasAnyRole(adminRoles)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已登录
|
||||
*/
|
||||
isLoggedIn(): boolean {
|
||||
return this.userStore.isLoggedIn && !!this.userStore.userInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色列表
|
||||
*/
|
||||
getUserRoles(): string[] {
|
||||
return this.userStore.userInfo?.roles?.map(r => r.role_code) || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限列表
|
||||
*/
|
||||
getUserPermissions(): string[] {
|
||||
return this.userStore.userInfo?.permissions || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否可以访问指定路径
|
||||
*/
|
||||
canAccessPath(path: string, requiredRoles?: string[], requiredPermissions?: string[]): boolean {
|
||||
if (!this.isLoggedIn()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果没有指定权限要求,默认允许访问
|
||||
if (!requiredRoles && !requiredPermissions) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
if (!this.hasAnyRole(requiredRoles)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查具体权限
|
||||
if (requiredPermissions && requiredPermissions.length > 0) {
|
||||
if (!this.hasAnyPermission(requiredPermissions)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局权限管理实例
|
||||
export const permissionManager = new PermissionManager()
|
||||
|
||||
// 便捷的权限检查函数
|
||||
export const usePermission = () => {
|
||||
return {
|
||||
hasRole: (role: string) => permissionManager.hasRole(role),
|
||||
hasPermission: (permission: string) => permissionManager.hasPermission(permission),
|
||||
hasAnyRole: (roles: string[]) => permissionManager.hasAnyRole(roles),
|
||||
hasAnyPermission: (permissions: string[]) => permissionManager.hasAnyPermission(permissions),
|
||||
isAdmin: () => permissionManager.isAdmin(),
|
||||
isLoggedIn: () => permissionManager.isLoggedIn(),
|
||||
canAccessPath: (path: string, requiredRoles?: string[], requiredPermissions?: string[]) =>
|
||||
permissionManager.canAccessPath(path, requiredRoles, requiredPermissions)
|
||||
}
|
||||
}
|
||||
|
||||
// Vue 3 组合式 API 权限检查 Hook
|
||||
export const usePermissionCheck = () => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
return {
|
||||
// 响应式权限检查
|
||||
hasRole: (role: string) => computed(() => permissionManager.hasRole(role)),
|
||||
hasPermission: (permission: string) => computed(() => permissionManager.hasPermission(permission)),
|
||||
hasAnyRole: (roles: string[]) => computed(() => permissionManager.hasAnyRole(roles)),
|
||||
hasAnyPermission: (permissions: string[]) => computed(() => permissionManager.hasAnyPermission(permissions)),
|
||||
isAdmin: computed(() => permissionManager.isAdmin()),
|
||||
isLoggedIn: computed(() => permissionManager.isLoggedIn()),
|
||||
|
||||
// 用户信息
|
||||
userRoles: computed(() => permissionManager.getUserRoles()),
|
||||
userPermissions: computed(() => permissionManager.getUserPermissions()),
|
||||
userInfo: computed(() => userStore.userInfo)
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||||
import { handleError } from './hertz_error_handler'
|
||||
|
||||
// 请求配置接口
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
showLoading?: boolean
|
||||
showError?: boolean
|
||||
metadata?: {
|
||||
requestId: string
|
||||
timestamp: string
|
||||
}
|
||||
}
|
||||
|
||||
// 响应数据接口
|
||||
interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
success?: boolean
|
||||
}
|
||||
|
||||
// 请求拦截器配置
|
||||
const requestInterceptor = {
|
||||
onFulfilled: (config: RequestConfig) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
const requestId = Math.random().toString(36).substr(2, 9)
|
||||
|
||||
// 简化日志,只在开发环境显示关键信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`)
|
||||
}
|
||||
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 如果是FormData,删除Content-Type让浏览器自动设置
|
||||
if (config.data instanceof FormData) {
|
||||
if (config.headers && 'Content-Type' in config.headers) {
|
||||
delete config.headers['Content-Type']
|
||||
}
|
||||
console.log('📦 检测到FormData,移除Content-Type让浏览器自动设置')
|
||||
}
|
||||
|
||||
// 显示loading
|
||||
if (config.showLoading !== false) {
|
||||
// 这里可以添加loading显示逻辑
|
||||
}
|
||||
|
||||
// 将requestId添加到config中,用于响应时匹配
|
||||
config.metadata = { requestId, timestamp }
|
||||
return config as InternalAxiosRequestConfig
|
||||
},
|
||||
onRejected: (error: any) => {
|
||||
console.error('❌ 请求错误:', error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 响应拦截器配置
|
||||
const responseInterceptor = {
|
||||
onFulfilled: (response: AxiosResponse) => {
|
||||
const requestTimestamp = (response.config as any).metadata?.timestamp
|
||||
const duration = requestTimestamp ? Date.now() - new Date(requestTimestamp).getTime() : 0
|
||||
|
||||
// 简化日志,只在开发环境显示关键信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`✅ ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url} (${duration}ms)`)
|
||||
}
|
||||
|
||||
// 统一处理响应数据
|
||||
if (response.data && typeof response.data === 'object') {
|
||||
// 如果后端返回的是标准格式 {code, message, data}
|
||||
if ('code' in response.data) {
|
||||
// 标准API响应格式处理
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
onRejected: (error: any) => {
|
||||
const requestTimestamp = (error.config as any)?.metadata?.timestamp
|
||||
const duration = requestTimestamp ? Date.now() - new Date(requestTimestamp).getTime() : 0
|
||||
|
||||
// 简化错误日志
|
||||
console.error(`❌ ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url} (${duration}ms)`)
|
||||
console.error('错误信息:', error.response?.data?.message || error.message)
|
||||
|
||||
// 使用统一错误处理器(支持按请求关闭全局错误提示)
|
||||
const showError = (error.config as any)?.showError
|
||||
if (showError !== false) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
// 特殊处理401错误
|
||||
if (error.response?.status === 401) {
|
||||
console.warn('🔒 未授权,清除token')
|
||||
localStorage.removeItem('token')
|
||||
// 可以在这里跳转到登录页
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
class HertzRequest {
|
||||
private instance: AxiosInstance
|
||||
|
||||
constructor(config: AxiosRequestConfig) {
|
||||
// 在开发环境中使用空字符串以便Vite代理正常工作
|
||||
// 在生产环境中使用完整的API地址
|
||||
const isDev = import.meta.env.DEV
|
||||
const baseURL = isDev ? '' : (import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000')
|
||||
console.log('🔧 创建axios实例 - isDev:', isDev)
|
||||
console.log('🔧 创建axios实例 - baseURL:', baseURL)
|
||||
console.log('🔧 环境变量 VITE_API_BASE_URL:', import.meta.env.VITE_API_BASE_URL)
|
||||
|
||||
this.instance = axios.create({
|
||||
baseURL,
|
||||
timeout: 10000,
|
||||
// 不设置默认Content-Type,让每个请求根据数据类型自动设置
|
||||
...config
|
||||
})
|
||||
|
||||
// 添加请求拦截器
|
||||
this.instance.interceptors.request.use(
|
||||
requestInterceptor.onFulfilled,
|
||||
requestInterceptor.onRejected
|
||||
)
|
||||
|
||||
// 添加响应拦截器
|
||||
this.instance.interceptors.response.use(
|
||||
responseInterceptor.onFulfilled,
|
||||
responseInterceptor.onRejected
|
||||
)
|
||||
}
|
||||
|
||||
// GET请求
|
||||
get<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.get(url, config).then(res => res.data)
|
||||
}
|
||||
|
||||
// POST请求
|
||||
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
||||
// 如果不是FormData,设置Content-Type为application/json
|
||||
const finalConfig = { ...config }
|
||||
if (!(data instanceof FormData)) {
|
||||
finalConfig.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...finalConfig.headers
|
||||
}
|
||||
}
|
||||
return this.instance.post(url, data, finalConfig).then(res => res.data)
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
||||
// 如果不是FormData,设置Content-Type为application/json
|
||||
const finalConfig = { ...config }
|
||||
if (!(data instanceof FormData)) {
|
||||
finalConfig.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...finalConfig.headers
|
||||
}
|
||||
}
|
||||
return this.instance.put(url, data, finalConfig).then(res => res.data)
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.delete(url, config).then(res => res.data)
|
||||
}
|
||||
|
||||
// PATCH请求
|
||||
patch<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.patch(url, data, config).then(res => res.data)
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
upload<T = any>(url: string, formData: FormData, config?: RequestConfig): Promise<T> {
|
||||
// 不要手动设置Content-Type,让浏览器自动设置,这样会包含正确的boundary
|
||||
return this.instance.post(url, formData, {
|
||||
...config,
|
||||
headers: {
|
||||
// 不设置Content-Type,让浏览器自动设置multipart/form-data的header
|
||||
...config?.headers
|
||||
}
|
||||
}).then(res => res.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const request = new HertzRequest({})
|
||||
|
||||
// 导出类和配置接口
|
||||
export { HertzRequest }
|
||||
export type { RequestConfig, ApiResponse }
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* 路由工具函数
|
||||
* 用于动态路由相关的辅助功能
|
||||
*/
|
||||
|
||||
// 获取views目录下的所有Vue文件
|
||||
export const getViewFiles = () => {
|
||||
const viewsContext = import.meta.glob('@/views/*.vue')
|
||||
return Object.keys(viewsContext).map(path => path.split('/').pop())
|
||||
}
|
||||
|
||||
// 从文件名生成路由名称
|
||||
export const generateRouteName = (fileName: string): string => {
|
||||
return fileName.replace('.vue', '')
|
||||
}
|
||||
|
||||
// 从文件名生成路由路径
|
||||
export const generateRoutePath = (fileName: string): string => {
|
||||
const routeName = generateRouteName(fileName)
|
||||
let routePath = `/${routeName.toLowerCase()}`
|
||||
|
||||
// 处理特殊命名(驼峰转短横线)
|
||||
if (routeName !== routeName.toLowerCase()) {
|
||||
routePath = `/${routeName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')}`
|
||||
}
|
||||
|
||||
return routePath
|
||||
}
|
||||
|
||||
// 生成路由标题
|
||||
export const generateRouteTitle = (routeName: string): string => {
|
||||
const titleMap: Record<string, string> = {
|
||||
Dashboard: '仪表板',
|
||||
User: '用户管理',
|
||||
Profile: '个人资料',
|
||||
Settings: '系统设置',
|
||||
Test: '样式测试',
|
||||
WebSocketTest: 'WebSocket测试',
|
||||
NotFound: '页面未找到',
|
||||
}
|
||||
|
||||
return titleMap[routeName] || routeName
|
||||
}
|
||||
|
||||
// 判断路由是否需要认证
|
||||
export const shouldRequireAuth = (routeName: string): boolean => {
|
||||
const publicRoutes = ['Test', 'WebSocketTest']
|
||||
return !(
|
||||
publicRoutes.includes(routeName) || // 公开路由列表
|
||||
routeName.startsWith('Demo') // Demo开头的页面不需要认证
|
||||
)
|
||||
}
|
||||
|
||||
// 获取公开路由列表
|
||||
export const getPublicRoutes = (): string[] => {
|
||||
return ['Test', 'WebSocketTest', 'Demo'] // 可以添加更多公开路由
|
||||
}
|
||||
|
||||
// 打印路由调试信息
|
||||
export const debugRoutes = () => {
|
||||
const viewFiles = getViewFiles()
|
||||
const fixedFiles = ['Home.vue', 'Login.vue']
|
||||
const dynamicFiles = viewFiles.filter(file => !fixedFiles.includes(file) && file !== 'NotFound.vue')
|
||||
|
||||
console.log('🔍 路由调试信息:')
|
||||
console.log('📁 所有视图文件:', viewFiles)
|
||||
console.log('🔒 固定路由文件:', fixedFiles)
|
||||
console.log('🚀 动态路由文件:', dynamicFiles)
|
||||
|
||||
const publicRoutes = getPublicRoutes()
|
||||
console.log('🔓 公开路由 (不需要认证):', publicRoutes)
|
||||
|
||||
console.log('\n📋 动态路由配置:')
|
||||
dynamicFiles.forEach(file => {
|
||||
const routeName = generateRouteName(file)
|
||||
const routePath = generateRoutePath(file)
|
||||
const title = generateRouteTitle(routeName)
|
||||
const requiresAuth = shouldRequireAuth(routeName)
|
||||
const isPublic = !requiresAuth
|
||||
|
||||
console.log(` ${file} → ${routePath} (${title}) ${isPublic ? '🔓' : '🔒'}`)
|
||||
})
|
||||
|
||||
console.log('\n🎯 Demo页面特殊说明:')
|
||||
console.log(' - Demo开头的页面不需要认证 (Demo.vue, DemoPage.vue等)')
|
||||
console.log(' - 可以直接访问 /demo 路径')
|
||||
}
|
||||
|
||||
// 在开发环境中自动调用调试函数
|
||||
if (import.meta.env.DEV) {
|
||||
debugRoutes()
|
||||
}
|
||||
|
||||
// 提供全局访问的路由信息查看函数
|
||||
export const showRoutesInfo = () => {
|
||||
console.log('🚀 Hertz Admin 路由配置信息:')
|
||||
console.log('📋 完整路由列表:')
|
||||
|
||||
// 注意: 这里需要从路由实例中获取真实数据
|
||||
// 由于路由工具函数在路由配置之前加载,这里提供的是示例数据
|
||||
// 实际的动态路由信息会在项目启动时通过logRouteInfo()函数显示
|
||||
|
||||
console.log('\n🔒 固定路由 (需要手动配置):')
|
||||
console.log(' 🔒 / → Home (首页)')
|
||||
console.log(' 🔓 /login → Login (登录)')
|
||||
|
||||
console.log('\n🚀 动态路由 (自动生成):')
|
||||
console.log(' 🔒 /dashboard → Dashboard (仪表板)')
|
||||
console.log(' 🔒 /user → User (用户管理)')
|
||||
console.log(' 🔒 /profile → Profile (个人资料)')
|
||||
console.log(' 🔒 /settings → Settings (系统设置)')
|
||||
console.log(' 🔓 /test → Test (样式测试)')
|
||||
console.log(' 🔓 /websocket-test → WebSocketTest (WebSocket测试)')
|
||||
console.log(' 🔓 /demo → Demo (动态路由演示)')
|
||||
|
||||
console.log('\n❓ 404路由:')
|
||||
console.log(' ❓ /:pathMatch(.*)* → NotFound (页面未找到)')
|
||||
|
||||
console.log('\n📖 访问说明:')
|
||||
console.log(' 🔓 公开路由: 可以直接访问,不需要登录')
|
||||
console.log(' 🔒 私有路由: 需要登录后才能访问')
|
||||
console.log(' 💡 提示: 可以在浏览器中直接访问这些路径')
|
||||
|
||||
console.log('\n🌐 可用链接:')
|
||||
console.log(' http://localhost:3000/ - 首页 (需要登录)')
|
||||
console.log(' http://localhost:3000/login - 登录页面')
|
||||
console.log(' http://localhost:3000/dashboard - 仪表板 (需要登录)')
|
||||
console.log(' http://localhost:3000/user - 用户管理 (需要登录)')
|
||||
console.log(' http://localhost:3000/profile - 个人资料 (需要登录)')
|
||||
console.log(' http://localhost:3000/settings - 系统设置 (需要登录)')
|
||||
console.log(' http://localhost:3000/test - 样式测试 (公开)')
|
||||
console.log(' http://localhost:3000/websocket-test - WebSocket测试 (公开)')
|
||||
console.log(' http://localhost:3000/demo - 动态路由演示 (公开)')
|
||||
console.log(' http://localhost:3000/any-other-path - 404页面 (公开)')
|
||||
|
||||
console.log('\n✅ 路由配置加载完成!')
|
||||
console.log('💡 提示: 启动项目后会在控制台看到真正的动态路由信息')
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* URL处理工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取完整的文件URL
|
||||
* @param relativePath 相对路径,如 /media/detection/original/xxx.jpg
|
||||
* @returns 完整的URL
|
||||
*/
|
||||
export function getFullFileUrl(relativePath: string): string {
|
||||
if (!relativePath) {
|
||||
console.warn('⚠️ 文件路径为空')
|
||||
return ''
|
||||
}
|
||||
|
||||
// 如果已经是完整URL,直接返回
|
||||
if (relativePath.startsWith('http://') || relativePath.startsWith('https://')) {
|
||||
return relativePath
|
||||
}
|
||||
|
||||
// 在开发环境中,使用相对路径(通过Vite代理)
|
||||
if (import.meta.env.DEV) {
|
||||
return relativePath
|
||||
}
|
||||
|
||||
// 在生产环境中,拼接完整的URL
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
|
||||
return `${baseURL}${relativePath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API基础URL
|
||||
* @returns API基础URL
|
||||
*/
|
||||
export function getApiBaseUrl(): string {
|
||||
if (import.meta.env.DEV) {
|
||||
return '' // 开发环境使用空字符串,通过Vite代理
|
||||
}
|
||||
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取媒体文件基础URL
|
||||
* @returns 媒体文件基础URL
|
||||
*/
|
||||
export function getMediaBaseUrl(): string {
|
||||
if (import.meta.env.DEV) {
|
||||
return '' // 开发环境使用空字符串,通过Vite代理
|
||||
}
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
|
||||
return baseURL.replace('/api', '') // 移除/api后缀
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否可访问
|
||||
* @param url 要检查的URL
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
export async function checkUrlAccessibility(url: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' })
|
||||
return response.ok
|
||||
} catch (error) {
|
||||
console.error('❌ URL访问检查失败:', url, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param bytes 字节数
|
||||
* @returns 格式化后的文件大小
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @param filename 文件名
|
||||
* @returns 文件扩展名
|
||||
*/
|
||||
export function getFileExtension(filename: string): string {
|
||||
return filename.split('.').pop()?.toLowerCase() || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为图片文件
|
||||
* @param filename 文件名或URL
|
||||
* @returns 是否为图片文件
|
||||
*/
|
||||
export function isImageFile(filename: string): boolean {
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
|
||||
const extension = getFileExtension(filename)
|
||||
return imageExtensions.includes(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为视频文件
|
||||
* @param filename 文件名或URL
|
||||
* @returns 是否为视频文件
|
||||
*/
|
||||
export function isVideoFile(filename: string): boolean {
|
||||
const videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv']
|
||||
const extension = getFileExtension(filename)
|
||||
return videoExtensions.includes(extension)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user