添加数据库文件
This commit is contained in:
25
hertz_server_diango_ui/.editorconfig
Normal file
25
hertz_server_diango_ui/.editorconfig
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 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
Normal file
24
hertz_server_diango_ui/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 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?
|
||||||
29396
hertz_server_diango_ui/.vite/deps/@ant-design_icons-vue.js
Normal file
29396
hertz_server_diango_ui/.vite/deps/@ant-design_icons-vue.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
109
hertz_server_diango_ui/.vite/deps/_metadata.json
Normal file
109
hertz_server_diango_ui/.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
275
hertz_server_diango_ui/.vite/deps/ant-design-vue.js
Normal file
275
hertz_server_diango_ui/.vite/deps/ant-design-vue.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
7
hertz_server_diango_ui/.vite/deps/ant-design-vue.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/ant-design-vue.js.map
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
275
hertz_server_diango_ui/.vite/deps/ant-design-vue_es.js
Normal file
275
hertz_server_diango_ui/.vite/deps/ant-design-vue_es.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
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
2601
hertz_server_diango_ui/.vite/deps/axios.js
Normal file
2601
hertz_server_diango_ui/.vite/deps/axios.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/axios.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/axios.js.map
Normal file
File diff suppressed because one or more lines are too long
162
hertz_server_diango_ui/.vite/deps/chunk-AYVSL3LM.js
Normal file
162
hertz_server_diango_ui/.vite/deps/chunk-AYVSL3LM.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// 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
|
||||||
7
hertz_server_diango_ui/.vite/deps/chunk-AYVSL3LM.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-AYVSL3LM.js.map
Normal file
File diff suppressed because one or more lines are too long
220
hertz_server_diango_ui/.vite/deps/chunk-F5SAIAJ6.js
Normal file
220
hertz_server_diango_ui/.vite/deps/chunk-F5SAIAJ6.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
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
|
||||||
7
hertz_server_diango_ui/.vite/deps/chunk-F5SAIAJ6.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-F5SAIAJ6.js.map
Normal file
File diff suppressed because one or more lines are too long
68
hertz_server_diango_ui/.vite/deps/chunk-IJSSJBZ4.js
Normal file
68
hertz_server_diango_ui/.vite/deps/chunk-IJSSJBZ4.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// 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
|
||||||
7
hertz_server_diango_ui/.vite/deps/chunk-IJSSJBZ4.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-IJSSJBZ4.js.map
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
42
hertz_server_diango_ui/.vite/deps/chunk-PR4QN5HX.js
Normal file
42
hertz_server_diango_ui/.vite/deps/chunk-PR4QN5HX.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
7
hertz_server_diango_ui/.vite/deps/chunk-PR4QN5HX.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-PR4QN5HX.js.map
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
61912
hertz_server_diango_ui/.vite/deps/chunk-UD4KTM7R.js
Normal file
61912
hertz_server_diango_ui/.vite/deps/chunk-UD4KTM7R.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/chunk-UD4KTM7R.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-UD4KTM7R.js.map
Normal file
File diff suppressed because one or more lines are too long
288
hertz_server_diango_ui/.vite/deps/chunk-XCUFKJYR.js
Normal file
288
hertz_server_diango_ui/.vite/deps/chunk-XCUFKJYR.js
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
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
|
||||||
7
hertz_server_diango_ui/.vite/deps/chunk-XCUFKJYR.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-XCUFKJYR.js.map
Normal file
File diff suppressed because one or more lines are too long
12683
hertz_server_diango_ui/.vite/deps/chunk-Y7TKRIWE.js
Normal file
12683
hertz_server_diango_ui/.vite/deps/chunk-Y7TKRIWE.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/chunk-Y7TKRIWE.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-Y7TKRIWE.js.map
Normal file
File diff suppressed because one or more lines are too long
20
hertz_server_diango_ui/.vite/deps/chunk-YODRZMZT.js
Normal file
20
hertz_server_diango_ui/.vite/deps/chunk-YODRZMZT.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// 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
|
||||||
7
hertz_server_diango_ui/.vite/deps/chunk-YODRZMZT.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-YODRZMZT.js.map
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
3568
hertz_server_diango_ui/.vite/deps/chunk-ZC474HKL.js
Normal file
3568
hertz_server_diango_ui/.vite/deps/chunk-ZC474HKL.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/chunk-ZC474HKL.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/chunk-ZC474HKL.js.map
Normal file
File diff suppressed because one or more lines are too long
5
hertz_server_diango_ui/.vite/deps/dayjs.js
Normal file
5
hertz_server_diango_ui/.vite/deps/dayjs.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import {
|
||||||
|
require_dayjs_min
|
||||||
|
} from "./chunk-XCUFKJYR.js";
|
||||||
|
import "./chunk-PR4QN5HX.js";
|
||||||
|
export default require_dayjs_min();
|
||||||
7
hertz_server_diango_ui/.vite/deps/dayjs.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/dayjs.js.map
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
2387
hertz_server_diango_ui/.vite/deps/jszip.js
Normal file
2387
hertz_server_diango_ui/.vite/deps/jszip.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/jszip.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/jszip.js.map
Normal file
File diff suppressed because one or more lines are too long
3
hertz_server_diango_ui/.vite/deps/package.json
Normal file
3
hertz_server_diango_ui/.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
5892
hertz_server_diango_ui/.vite/deps/pinia.js
Normal file
5892
hertz_server_diango_ui/.vite/deps/pinia.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/pinia.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/pinia.js.map
Normal file
File diff suppressed because one or more lines are too long
5317
hertz_server_diango_ui/.vite/deps/vue-i18n.js
Normal file
5317
hertz_server_diango_ui/.vite/deps/vue-i18n.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/vue-i18n.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/vue-i18n.js.map
Normal file
File diff suppressed because one or more lines are too long
2782
hertz_server_diango_ui/.vite/deps/vue-router.js
Normal file
2782
hertz_server_diango_ui/.vite/deps/vue-router.js
Normal file
File diff suppressed because it is too large
Load Diff
7
hertz_server_diango_ui/.vite/deps/vue-router.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/vue-router.js.map
Normal file
File diff suppressed because one or more lines are too long
343
hertz_server_diango_ui/.vite/deps/vue.js
Normal file
343
hertz_server_diango_ui/.vite/deps/vue.js
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
7
hertz_server_diango_ui/.vite/deps/vue.js.map
Normal file
7
hertz_server_diango_ui/.vite/deps/vue.js.map
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
137
hertz_server_diango_ui/README.md
Normal file
137
hertz_server_diango_ui/README.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<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
Normal file
80
hertz_server_diango_ui/components.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/* 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']
|
||||||
|
}
|
||||||
|
}
|
||||||
81
hertz_server_diango_ui/eslint.config.js
Normal file
81
hertz_server_diango_ui/eslint.config.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
13
hertz_server_diango_ui/index.html
Normal file
13
hertz_server_diango_ui/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!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
Normal file
5692
hertz_server_diango_ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
hertz_server_diango_ui/package.json
Normal file
41
hertz_server_diango_ui/package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"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
hertz_server_diango_ui/public/models/manifest.json
Normal file
1
hertz_server_diango_ui/public/models/manifest.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
1
hertz_server_diango_ui/public/vite.svg
Normal file
1
hertz_server_diango_ui/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
85
hertz_server_diango_ui/src/App.vue
Normal file
85
hertz_server_diango_ui/src/App.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<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>
|
||||||
96
hertz_server_diango_ui/src/api/ai.ts
Normal file
96
hertz_server_diango_ui/src/api/ai.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
47
hertz_server_diango_ui/src/api/auth.ts
Normal file
47
hertz_server_diango_ui/src/api/auth.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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/')
|
||||||
|
}
|
||||||
89
hertz_server_diango_ui/src/api/captcha.ts
Normal file
89
hertz_server_diango_ui/src/api/captcha.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
393
hertz_server_diango_ui/src/api/dashboard.ts
Normal file
393
hertz_server_diango_ui/src/api/dashboard.ts
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
93
hertz_server_diango_ui/src/api/department.ts
Normal file
93
hertz_server_diango_ui/src/api/department.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
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/')
|
||||||
|
}
|
||||||
|
}
|
||||||
16
hertz_server_diango_ui/src/api/index.ts
Normal file
16
hertz_server_diango_ui/src/api/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// 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'
|
||||||
173
hertz_server_diango_ui/src/api/knowledge.ts
Normal file
173
hertz_server_diango_ui/src/api/knowledge.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
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/`)
|
||||||
|
},
|
||||||
|
}
|
||||||
110
hertz_server_diango_ui/src/api/log.ts
Normal file
110
hertz_server_diango_ui/src/api/log.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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 } })
|
||||||
|
},
|
||||||
|
}
|
||||||
361
hertz_server_diango_ui/src/api/menu.ts
Normal file
361
hertz_server_diango_ui/src/api/menu.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
87
hertz_server_diango_ui/src/api/notice_user.ts
Normal file
87
hertz_server_diango_ui/src/api/notice_user.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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 }),
|
||||||
|
}
|
||||||
31
hertz_server_diango_ui/src/api/password.ts
Normal file
31
hertz_server_diango_ui/src/api/password.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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 })
|
||||||
|
}
|
||||||
130
hertz_server_diango_ui/src/api/role.ts
Normal file
130
hertz_server_diango_ui/src/api/role.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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/')
|
||||||
|
}
|
||||||
|
}
|
||||||
114
hertz_server_diango_ui/src/api/system_monitor.ts
Normal file
114
hertz_server_diango_ui/src/api/system_monitor.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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/'),
|
||||||
|
}
|
||||||
115
hertz_server_diango_ui/src/api/user.ts
Normal file
115
hertz_server_diango_ui/src/api/user.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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/')
|
||||||
|
}
|
||||||
|
}
|
||||||
428
hertz_server_diango_ui/src/api/yolo.ts
Normal file
428
hertz_server_diango_ui/src/api/yolo.ts
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
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
hertz_server_diango_ui/src/assets/vue.svg
Normal file
1
hertz_server_diango_ui/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 496 B |
55
hertz_server_diango_ui/src/components/HelloWorld.vue
Normal file
55
hertz_server_diango_ui/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<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>
|
||||||
159
hertz_server_diango_ui/src/locales/en-US.ts
Normal file
159
hertz_server_diango_ui/src/locales/en-US.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
}
|
||||||
18
hertz_server_diango_ui/src/locales/index.ts
Normal file
18
hertz_server_diango_ui/src/locales/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
172
hertz_server_diango_ui/src/locales/zh-CN.ts
Normal file
172
hertz_server_diango_ui/src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
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: '系统配置错误,请联系管理员',
|
||||||
|
},
|
||||||
|
}
|
||||||
47
hertz_server_diango_ui/src/main.ts
Normal file
47
hertz_server_diango_ui/src/main.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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')
|
||||||
69
hertz_server_diango_ui/src/outer_src/api/ai.ts
Normal file
69
hertz_server_diango_ui/src/outer_src/api/ai.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
231
hertz_server_diango_ui/src/outer_src/router/user_menu_ai.ts
Normal file
231
hertz_server_diango_ui/src/outer_src/router/user_menu_ai.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
261
hertz_server_diango_ui/src/outer_src/views/user_pages/AiChat.vue
Normal file
261
hertz_server_diango_ui/src/outer_src/views/user_pages/AiChat.vue
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<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>
|
||||||
BIN
hertz_server_diango_ui/src/public/img/logo-蓝.png
Normal file
BIN
hertz_server_diango_ui/src/public/img/logo-蓝.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
424
hertz_server_diango_ui/src/router/admin_menu.ts
Normal file
424
hertz_server_diango_ui/src/router/admin_menu.ts
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
275
hertz_server_diango_ui/src/router/index.ts
Normal file
275
hertz_server_diango_ui/src/router/index.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
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;
|
||||||
183
hertz_server_diango_ui/src/router/user_menu_ai.ts
Normal file
183
hertz_server_diango_ui/src/router/user_menu_ai.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
98
hertz_server_diango_ui/src/stores/hertz_app.ts
Normal file
98
hertz_server_diango_ui/src/stores/hertz_app.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
101
hertz_server_diango_ui/src/stores/hertz_theme.ts
Normal file
101
hertz_server_diango_ui/src/stores/hertz_theme.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
246
hertz_server_diango_ui/src/stores/hertz_user.ts
Normal file
246
hertz_server_diango_ui/src/stores/hertz_user.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
79
hertz_server_diango_ui/src/style.css
Normal file
79
hertz_server_diango_ui/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
422
hertz_server_diango_ui/src/styles/index.scss
Normal file
422
hertz_server_diango_ui/src/styles/index.scss
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
// 全局样式入口文件
|
||||||
|
@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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
124
hertz_server_diango_ui/src/styles/variables.scss
Normal file
124
hertz_server_diango_ui/src/styles/variables.scss
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// 全局变量文件 - 简约现代风格
|
||||||
|
|
||||||
|
// 颜色系统
|
||||||
|
$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
Normal file
13
hertz_server_diango_ui/src/types/env.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/// <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
|
||||||
|
}
|
||||||
182
hertz_server_diango_ui/src/types/hertz_types.ts
Normal file
182
hertz_server_diango_ui/src/types/hertz_types.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// 通用响应类型
|
||||||
|
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
|
||||||
|
}
|
||||||
70
hertz_server_diango_ui/src/utils/hertz_captcha.ts
Normal file
70
hertz_server_diango_ui/src/utils/hertz_captcha.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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 }
|
||||||
87
hertz_server_diango_ui/src/utils/hertz_env.ts
Normal file
87
hertz_server_diango_ui/src/utils/hertz_env.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 环境变量检查工具
|
||||||
|
* 用于在开发环境中检查环境变量配置是否正确
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 检查环境变量配置
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
}
|
||||||
375
hertz_server_diango_ui/src/utils/hertz_error_handler.ts
Normal file
375
hertz_server_diango_ui/src/utils/hertz_error_handler.ts
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
154
hertz_server_diango_ui/src/utils/hertz_permission.ts
Normal file
154
hertz_server_diango_ui/src/utils/hertz_permission.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* 权限管理工具类
|
||||||
|
* 统一管理用户权限检查和菜单过滤逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
201
hertz_server_diango_ui/src/utils/hertz_request.ts
Normal file
201
hertz_server_diango_ui/src/utils/hertz_request.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
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 }
|
||||||
138
hertz_server_diango_ui/src/utils/hertz_router_utils.ts
Normal file
138
hertz_server_diango_ui/src/utils/hertz_router_utils.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 路由工具函数
|
||||||
|
* 用于动态路由相关的辅助功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 获取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('💡 提示: 启动项目后会在控制台看到真正的动态路由信息')
|
||||||
|
}
|
||||||
113
hertz_server_diango_ui/src/utils/hertz_url.ts
Normal file
113
hertz_server_diango_ui/src/utils/hertz_url.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
251
hertz_server_diango_ui/src/utils/hertz_utils.ts
Normal file
251
hertz_server_diango_ui/src/utils/hertz_utils.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { useAppStore } from '@/stores/hertz_app'
|
||||||
|
|
||||||
|
// 日期格式化
|
||||||
|
export const formatDate = (date: Date | string | number, format = 'YYYY-MM-DD HH:mm:ss') => {
|
||||||
|
const d = new Date(date)
|
||||||
|
|
||||||
|
if (isNaN(d.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace('YYYY', year.toString())
|
||||||
|
.replace('MM', month)
|
||||||
|
.replace('DD', day)
|
||||||
|
.replace('HH', hours)
|
||||||
|
.replace('mm', minutes)
|
||||||
|
.replace('ss', seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防抖函数
|
||||||
|
export const debounce = <T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
let timeoutId: NodeJS.Timeout
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
timeoutId = setTimeout(() => func(...args), delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节流函数
|
||||||
|
export const throttle = <T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
let lastCall = 0
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (now - lastCall >= delay) {
|
||||||
|
lastCall = now
|
||||||
|
func(...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 深拷贝
|
||||||
|
export const deepClone = <T>(obj: T): T => {
|
||||||
|
if (obj === null || typeof obj !== 'object') {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Date) {
|
||||||
|
return new Date(obj.getTime()) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
return obj.map(item => deepClone(item)) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const cloned = {} as T
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
cloned[key] = deepClone(obj[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数组去重
|
||||||
|
export const unique = <T>(arr: T[]): T[] => {
|
||||||
|
return Array.from(new Set(arr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取URL参数
|
||||||
|
export const getUrlParam = (name: string, url?: string): string | null => {
|
||||||
|
const searchUrl = url || window.location.search
|
||||||
|
const params = new URLSearchParams(searchUrl)
|
||||||
|
return params.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置URL参数
|
||||||
|
export const setUrlParam = (name: string, value: string, url?: string): string => {
|
||||||
|
const searchUrl = url || window.location.search
|
||||||
|
const params = new URLSearchParams(searchUrl)
|
||||||
|
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
params.delete(name)
|
||||||
|
} else {
|
||||||
|
params.set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
export const copyToClipboard = async (text: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
} else {
|
||||||
|
// 降级处理
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.left = '-999999px'
|
||||||
|
textArea.style.top = '-999999px'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
|
||||||
|
const successful = document.execCommand('copy')
|
||||||
|
textArea.remove()
|
||||||
|
|
||||||
|
if (!successful) {
|
||||||
|
throw new Error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
export const downloadFile = (url: string, filename?: string) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = filename || ''
|
||||||
|
link.style.display = 'none'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
export const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', '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]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证邮箱格式
|
||||||
|
export const isValidEmail = (email: string): boolean => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
return emailRegex.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证手机号格式(中国大陆)
|
||||||
|
export const isValidPhone = (phone: string): boolean => {
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/
|
||||||
|
return phoneRegex.test(phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证身份证号
|
||||||
|
export const isValidIdCard = (idCard: string): boolean => {
|
||||||
|
const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||||
|
return idCardRegex.test(idCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机字符串
|
||||||
|
export const generateRandomString = (length: number = 8): string => {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
let result = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待函数
|
||||||
|
export const sleep = (ms: number): Promise<void> => {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取浏览器信息
|
||||||
|
export const getBrowserInfo = () => {
|
||||||
|
const userAgent = navigator.userAgent
|
||||||
|
const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor)
|
||||||
|
const isFirefox = /Firefox/.test(userAgent)
|
||||||
|
const isSafari = /Safari/.test(userAgent) && /Apple Computer/.test(navigator.vendor)
|
||||||
|
const isEdge = /Edg/.test(userAgent)
|
||||||
|
const isIE = /MSIE|Trident/.test(userAgent)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isChrome,
|
||||||
|
isFirefox,
|
||||||
|
isSafari,
|
||||||
|
isEdge,
|
||||||
|
isIE,
|
||||||
|
userAgent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地存储封装
|
||||||
|
export const storage = {
|
||||||
|
get: <T>(key: string, defaultValue?: T): T | null => {
|
||||||
|
try {
|
||||||
|
const item = localStorage.getItem(key)
|
||||||
|
return item ? JSON.parse(item) : (defaultValue ?? null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取本地存储失败 (${key}):`, error)
|
||||||
|
return defaultValue ?? null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set: <T>(key: string, value: T): void => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`设置本地存储失败 (${key}):`, error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: (key: string): void => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`删除本地存储失败 (${key}):`, error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: (): void => {
|
||||||
|
try {
|
||||||
|
localStorage.clear()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清空本地存储失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
112
hertz_server_diango_ui/src/utils/menu_mapping.ts
Normal file
112
hertz_server_diango_ui/src/utils/menu_mapping.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { menuApi, type Menu } from '@/api/menu'
|
||||||
|
|
||||||
|
// 菜单key和菜单ID的映射关系
|
||||||
|
let menuKeyToIdMap: Map<string, number> = new Map()
|
||||||
|
let menuIdToKeyMap: Map<number, string> = new Map()
|
||||||
|
let isInitialized = false
|
||||||
|
|
||||||
|
// 菜单key和菜单code的映射关系(用于建立映射)
|
||||||
|
const MENU_KEY_TO_CODE_MAP: { [key: string]: string } = {
|
||||||
|
'dashboard': 'dashboard',
|
||||||
|
'user-management': 'user_management',
|
||||||
|
'department-management': 'department_management',
|
||||||
|
'menu-management': 'menu_management',
|
||||||
|
'teacher': 'role_management'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化菜单映射
|
||||||
|
*/
|
||||||
|
export const initializeMenuMapping = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// 获取菜单树数据
|
||||||
|
const response = await menuApi.getMenuTree()
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
// 清空现有映射
|
||||||
|
menuKeyToIdMap.clear()
|
||||||
|
|
||||||
|
// 递归处理菜单树
|
||||||
|
const processMenuTree = (menus: Menu[]) => {
|
||||||
|
menus.forEach(menu => {
|
||||||
|
if (menu.key && menu.id) {
|
||||||
|
menuKeyToIdMap.set(menu.key, menu.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子菜单
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
processMenuTree(menu.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
processMenuTree(response.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化菜单映射时发生错误:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归构建菜单映射关系
|
||||||
|
*/
|
||||||
|
const buildMenuMapping = (menus: Menu[]): void => {
|
||||||
|
menus.forEach(menu => {
|
||||||
|
// 根据menu_code找到对应的key
|
||||||
|
const menuKey = Object.keys(MENU_KEY_TO_CODE_MAP).find(
|
||||||
|
key => MENU_KEY_TO_CODE_MAP[key] === menu.menu_code
|
||||||
|
)
|
||||||
|
|
||||||
|
if (menuKey) {
|
||||||
|
menuKeyToIdMap.set(menuKey, menu.menu_id)
|
||||||
|
menuIdToKeyMap.set(menu.menu_id, menuKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子菜单
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
buildMenuMapping(menu.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据菜单key获取菜单ID
|
||||||
|
*/
|
||||||
|
export const getMenuIdByKey = (menuKey: string): number | undefined => {
|
||||||
|
return menuKeyToIdMap.get(menuKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据菜单ID获取菜单key
|
||||||
|
*/
|
||||||
|
export const getMenuKeyById = (menuId: number): string | undefined => {
|
||||||
|
return menuIdToKeyMap.get(menuId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否有指定菜单的权限
|
||||||
|
*/
|
||||||
|
export const hasMenuPermissionById = (menuKey: string, userMenuPermissions: number[]): boolean => {
|
||||||
|
const menuId = getMenuIdByKey(menuKey)
|
||||||
|
|
||||||
|
if (!menuId) {
|
||||||
|
// 降级策略:如果没有找到菜单映射,则允许显示(向后兼容)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return userMenuPermissions.includes(menuId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户有权限的菜单keys
|
||||||
|
*/
|
||||||
|
export const getPermittedMenuKeys = (userMenuPermissions: number[]): string[] => {
|
||||||
|
const permittedKeys: string[] = []
|
||||||
|
userMenuPermissions.forEach(menuId => {
|
||||||
|
const menuKey = getMenuKeyById(menuId)
|
||||||
|
if (menuKey) {
|
||||||
|
permittedKeys.push(menuKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return permittedKeys
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user