Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
B
boyueCEnd
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
游洁
boyueCEnd
Commits
5e6bcbb9
Commit
5e6bcbb9
authored
Dec 05, 2025
by
罗超
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
完成部分功能调整
parent
cbf236c6
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
510 additions
and
247 deletions
+510
-247
2.json
docs/2.json
+0
-218
App.vue
src/App.vue
+16
-1
base.css
src/assets/base.css
+11
-0
en.ts
src/i18n/locales/en.ts
+2
-0
vi.ts
src/i18n/locales/vi.ts
+2
-0
zh-CN.ts
src/i18n/locales/zh-CN.ts
+2
-0
zh-TW.ts
src/i18n/locales/zh-TW.ts
+2
-0
Footer.vue
src/layouts/components/Footer.vue
+73
-10
HeaderDestinationMenu.vue
src/layouts/components/HeaderDestinationMenu.vue
+57
-10
HeaderNavBar.vue
src/layouts/components/HeaderNavBar.vue
+38
-4
HeaderTopBar.vue
src/layouts/components/HeaderTopBar.vue
+2
-2
Headers.vue
src/layouts/components/Headers.vue
+29
-2
PlaceService.ts
src/services/PlaceService.ts
+50
-0
themeUtils.ts
src/utils/themeUtils.ts
+226
-0
No files found.
docs/2.json
deleted
100644 → 0
View file @
cbf236c6
{
"openapi"
:
"3.0.1"
,
"info"
:
{
"title"
:
"OTA平台项目"
,
"description"
:
""
,
"version"
:
"1.0.0"
},
"tags"
:
[
{
"name"
:
"PlaceServices"
}
],
"paths"
:
{
"/api/app/sys-management/place-services/tree"
:
{
"get"
:
{
"summary"
:
"获取地点树形结构(洲 -> 国家 -> 地点)"
,
"deprecated"
:
false
,
"description"
:
""
,
"tags"
:
[
"PlaceServices"
],
"parameters"
:
[
{
"name"
:
"__tenant"
,
"in"
:
"header"
,
"description"
:
"租户ID或租户名称(留空表示默认租户)"
,
"required"
:
false
,
"example"
:
""
,
"schema"
:
{
"type"
:
"string"
}
}
],
"responses"
:
{
"200"
:
{
"description"
:
"Success"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Acme.SysManagement.Application.Contracts.Dtos.Place.PlaceTreeQueryDto"
}
}
}
},
"headers"
:
{}
},
"400"
:
{
"description"
:
"Bad Request"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers"
:
{}
},
"401"
:
{
"description"
:
"Unauthorized"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers"
:
{}
},
"403"
:
{
"description"
:
"Forbidden"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers"
:
{}
},
"404"
:
{
"description"
:
"Not Found"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers"
:
{}
},
"500"
:
{
"description"
:
"Server Error"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers"
:
{}
},
"501"
:
{
"description"
:
"Server Error"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers"
:
{}
}
},
"security"
:
[]
}
}
},
"components"
:
{
"schemas"
:
{
"Volo.Abp.Http.RemoteServiceValidationErrorInfo"
:
{
"type"
:
"object"
,
"properties"
:
{
"message"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"members"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"string"
},
"nullable"
:
true
}
},
"additionalProperties"
:
false
},
"Volo.Abp.Http.RemoteServiceErrorInfo"
:
{
"type"
:
"object"
,
"properties"
:
{
"code"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"message"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"details"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"data"
:
{
"type"
:
"object"
,
"additionalProperties"
:
{
"type"
:
"string"
},
"properties"
:
{},
"nullable"
:
true
},
"validationErrors"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceValidationErrorInfo"
},
"nullable"
:
true
}
},
"additionalProperties"
:
false
},
"Acme.SysManagement.Application.Contracts.Dtos.Place.PlaceTreeEnum"
:
{
"enum"
:
[
"Continent"
,
"Country"
,
"Place"
],
"type"
:
"string"
,
"description"
:
"【枚举:Continent=1】
\r\n
【枚举:Country=2】
\r\n
【枚举:Place=3】
\r\n
"
},
"Volo.Abp.Http.RemoteServiceErrorResponse"
:
{
"type"
:
"object"
,
"properties"
:
{
"error"
:
{
"$ref"
:
"#/components/schemas/Volo.Abp.Http.RemoteServiceErrorInfo"
}
},
"additionalProperties"
:
false
},
"Acme.SysManagement.Application.Contracts.Dtos.Place.PlaceTreeQueryDto"
:
{
"type"
:
"object"
,
"properties"
:
{
"value"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"label"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"type"
:
{
"$ref"
:
"#/components/schemas/Acme.SysManagement.Application.Contracts.Dtos.Place.PlaceTreeEnum"
},
"children"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Acme.SysManagement.Application.Contracts.Dtos.Place.PlaceTreeQueryDto"
},
"nullable"
:
true
}
},
"additionalProperties"
:
false
}
},
"securitySchemes"
:
{}
},
"servers"
:
[]
}
\ No newline at end of file
src/App.vue
View file @
5e6bcbb9
...
@@ -7,6 +7,7 @@ import LanguageSwitcher from './components/common/LanguageSwitcher.vue'
...
@@ -7,6 +7,7 @@ import LanguageSwitcher from './components/common/LanguageSwitcher.vue'
import
{
configureArcoLocale
,
globalArcoLocale
}
from
'./i18n/arco'
import
{
configureArcoLocale
,
globalArcoLocale
}
from
'./i18n/arco'
import
{
useUserStore
}
from
'./stores/user'
import
{
useUserStore
}
from
'./stores/user'
import
{
useSystemConfigStore
}
from
'./stores/systemConfig'
import
{
useSystemConfigStore
}
from
'./stores/systemConfig'
import
{
loadThemeFromConfig
}
from
'./utils/themeUtils'
const
route
=
useRoute
()
const
route
=
useRoute
()
const
{
t
,
locale
}
=
useI18n
()
const
{
t
,
locale
}
=
useI18n
()
...
@@ -33,12 +34,26 @@ watch(
...
@@ -33,12 +34,26 @@ watch(
{
immediate
:
true
}
{
immediate
:
true
}
)
)
// 监听系统配置变化,动态应用配色
watch
(
()
=>
systemConfigStore
.
config
?.
webSiteColor
,
(
webSiteColor
)
=>
{
if
(
webSiteColor
)
{
loadThemeFromConfig
(
webSiteColor
)
}
},
{
immediate
:
true
}
)
onMounted
(
async
()
=>
{
onMounted
(
async
()
=>
{
try
{
try
{
// 初始化系统配置(在最开始执行)
// 初始化系统配置(在最开始执行)
await
systemConfigStore
.
initialize
(
location
.
hostname
)
await
systemConfigStore
.
initialize
(
location
.
hostname
)
//await systemConfigStore.getGroupInfoAsync()
// 应用配色(如果配置已加载)
if
(
systemConfigStore
.
config
?.
webSiteColor
)
{
loadThemeFromConfig
(
systemConfigStore
.
config
.
webSiteColor
)
}
// 初始化 Arco Design 国际化
// 初始化 Arco Design 国际化
await
configureArcoLocale
(
locale
.
value
)
await
configureArcoLocale
(
locale
.
value
)
...
...
src/assets/base.css
View file @
5e6bcbb9
...
@@ -30,3 +30,14 @@ body {
...
@@ -30,3 +30,14 @@ body {
.arco-dropdown
{
.arco-dropdown
{
padding
:
0
!important
;
padding
:
0
!important
;
}
}
.overlap
::before
{
display
:
block
;
content
:
""
;
height
:
100%
;
left
:
0
;
position
:
absolute
;
top
:
0
;
width
:
100%
;
background
:
linear-gradient
(
transparent
60%
,
rgba
(
0
,
0
,
0
,
.85
));
}
\ No newline at end of file
src/i18n/locales/en.ts
View file @
5e6bcbb9
...
@@ -112,6 +112,8 @@ export default {
...
@@ -112,6 +112,8 @@ export default {
header
:
{
header
:
{
destinations
:
'Destinations'
,
destinations
:
'Destinations'
,
destinationsFailed
:
'Failed to load destinations'
,
destinationsFailed
:
'Failed to load destinations'
,
recommond
:
'Recommend'
,
hotDestinations
:
'Hot Destinations'
,
},
},
place
:{
place
:{
all
:
'Explore all'
all
:
'Explore all'
...
...
src/i18n/locales/vi.ts
View file @
5e6bcbb9
...
@@ -112,6 +112,8 @@ export default {
...
@@ -112,6 +112,8 @@ export default {
header
:
{
header
:
{
destinations
:
'Điểm đến'
,
destinations
:
'Điểm đến'
,
destinationsFailed
:
'Không tải được danh sách điểm đến'
,
destinationsFailed
:
'Không tải được danh sách điểm đến'
,
recommond
:
'Giới thiệu'
,
hotDestinations
:
'Điểm đến nổi tiếng'
,
},
},
place
:{
place
:{
all
:
'Khám phá tất cả'
all
:
'Khám phá tất cả'
...
...
src/i18n/locales/zh-CN.ts
View file @
5e6bcbb9
...
@@ -112,6 +112,8 @@ export default {
...
@@ -112,6 +112,8 @@ export default {
header
:
{
header
:
{
destinations
:
'目的地'
,
destinations
:
'目的地'
,
destinationsFailed
:
'目的地数据加载失败'
,
destinationsFailed
:
'目的地数据加载失败'
,
recommond
:
'推荐'
,
hotDestinations
:
'热门目的地'
,
},
},
place
:{
place
:{
all
:
'探索全部'
all
:
'探索全部'
...
...
src/i18n/locales/zh-TW.ts
View file @
5e6bcbb9
...
@@ -112,6 +112,8 @@ export default {
...
@@ -112,6 +112,8 @@ export default {
header
:
{
header
:
{
destinations
:
'目的地'
,
destinations
:
'目的地'
,
destinationsFailed
:
'目的地資料載入失敗'
,
destinationsFailed
:
'目的地資料載入失敗'
,
recommond
:
'推薦'
,
hotDestinations
:
'熱門目的地'
,
},
},
place
:{
place
:{
all
:
'探索全部'
,
all
:
'探索全部'
,
...
...
src/layouts/components/Footer.vue
View file @
5e6bcbb9
<
template
>
<
template
>
<footer
class=
"footer
bg-gray-800
text-white"
>
<footer
class=
"footer text-white"
>
<!-- 顶部区域:社交媒体 + Newsletter -->
<!-- 顶部区域:社交媒体 + Newsletter -->
<div
class=
"footer-top border-b
border-gray-700
py-5"
>
<div
class=
"footer-top border-b py-5"
>
<div
class=
"footer-container py-8"
>
<div
class=
"footer-container py-8"
>
<div
class=
"flex flex-col md:flex-row items-center justify-between gap-6"
>
<div
class=
"flex flex-col md:flex-row items-center justify-between gap-6"
>
<!-- 社交媒体图标 -->
<!-- 社交媒体图标 -->
...
@@ -31,7 +31,7 @@
...
@@ -31,7 +31,7 @@
<p
class=
"text-sm font-medium mb-1"
>
<p
class=
"text-sm font-medium mb-1"
>
{{
t
(
'footer.newsletter.title'
)
}}
{{
t
(
'footer.newsletter.title'
)
}}
</p>
</p>
<p
class=
"text-xs
text-gray-400
"
>
<p
class=
"text-xs
footer-subtitle
"
>
{{
t
(
'footer.newsletter.subtitle'
)
}}
{{
t
(
'footer.newsletter.subtitle'
)
}}
</p>
</p>
</div>
</div>
...
@@ -46,7 +46,7 @@
...
@@ -46,7 +46,7 @@
{{
t
(
'footer.newsletter.subscribe'
)
}}
{{
t
(
'footer.newsletter.subscribe'
)
}}
</a-button>
</a-button>
</div>
</div>
<p
class=
"text-xs
text-gray-500
mt-2"
>
<p
class=
"text-xs
footer-agreement
mt-2"
>
{{
t
(
'footer.newsletter.agreement'
)
}}
{{
t
(
'footer.newsletter.agreement'
)
}}
</p>
</p>
</div>
</div>
...
@@ -55,7 +55,7 @@
...
@@ -55,7 +55,7 @@
</div>
</div>
<!-- 中间区域:链接列 -->
<!-- 中间区域:链接列 -->
<div
class=
"footer-middle border-b
border-gray-700
py-8"
>
<div
class=
"footer-middle border-b py-8"
>
<div
class=
"footer-container"
>
<div
class=
"footer-container"
>
<div
class=
"grid grid-cols-1 gap-8"
:class=
"[`md:grid-cols-$
{bottomNavs.length+1}`]">
<div
class=
"grid grid-cols-1 gap-8"
:class=
"[`md:grid-cols-$
{bottomNavs.length+1}`]">
...
@@ -63,7 +63,7 @@
...
@@ -63,7 +63,7 @@
<h3
class=
"font-semibold mb-4"
>
{{
t
(
item
.
navTitle
)
}}
</h3>
<h3
class=
"font-semibold mb-4"
>
{{
t
(
item
.
navTitle
)
}}
</h3>
<ul
class=
"space-y-2"
>
<ul
class=
"space-y-2"
>
<li
v-for=
"child in item.childList"
:key=
"child.id"
>
<li
v-for=
"child in item.childList"
:key=
"child.id"
>
<a
:href=
"child.navUrl??'javascript:void(0);'"
class=
"text-sm
text-gray-300 hover:text-white
transition-colors"
>
<a
:href=
"child.navUrl??'javascript:void(0);'"
class=
"text-sm
footer-link
transition-colors"
>
{{
t
(
child
.
navTitle
)
}}
{{
t
(
child
.
navTitle
)
}}
</a>
</a>
</li>
</li>
...
@@ -97,7 +97,7 @@
...
@@ -97,7 +97,7 @@
<div
class=
"footer-bottom py-4"
>
<div
class=
"footer-bottom py-4"
>
<div
class=
"footer-container"
>
<div
class=
"footer-container"
>
<div
class=
"flex flex-col md:flex-row items-center justify-between gap-4"
>
<div
class=
"flex flex-col md:flex-row items-center justify-between gap-4"
>
<div
class=
"text-sm
text-gray-400
"
>
<div
class=
"text-sm
footer-copyright
"
>
<span
v-if=
"systemConfigStore.config?.domainName"
>
<span
v-if=
"systemConfigStore.config?.domainName"
>
{{
systemConfigStore
.
config
.
domainName
}}
{{
systemConfigStore
.
config
.
domainName
}}
</span>
</span>
...
@@ -108,7 +108,7 @@
...
@@ -108,7 +108,7 @@
<a-button
<a-button
type=
"text"
type=
"text"
size=
"small"
size=
"small"
class=
"back-to-top
!text-gray-400 hover:!text-white
"
class=
"back-to-top
footer-back-to-top
"
@
click=
"scrollToTop"
@
click=
"scrollToTop"
>
>
<template
#
icon
>
<template
#
icon
>
...
@@ -243,7 +243,10 @@ const scrollToTop = () => {
...
@@ -243,7 +243,10 @@ const scrollToTop = () => {
<
style
scoped
lang=
"scss"
>
<
style
scoped
lang=
"scss"
>
.footer
{
.footer
{
background-color
:
#2d2d2d
;
// 使用动态配色:基于主色生成深色背景
// 如果主色较浅,使用深色背景;如果主色较深,使用稍浅的背景
background-color
:
rgb
(
var
(
--
arcoblue-8
));
color
:
white
;
}
}
.footer-container
{
.footer-container
{
...
@@ -252,6 +255,13 @@ const scrollToTop = () => {
...
@@ -252,6 +255,13 @@ const scrollToTop = () => {
padding
:
0
20px
;
padding
:
0
20px
;
}
}
// 顶部和中间区域的边框使用动态配色
.footer-top
,
.footer-middle
{
border-color
:
rgba
(
255
,
255
,
255
,
0
.1
);
}
// 社交媒体图标
.social-icon
{
.social-icon
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
...
@@ -260,6 +270,47 @@ const scrollToTop = () => {
...
@@ -260,6 +270,47 @@ const scrollToTop = () => {
height
:
32px
;
height
:
32px
;
color
:
white
;
color
:
white
;
cursor
:
pointer
;
cursor
:
pointer
;
transition
:
all
0
.2s
ease
;
&
:hover
{
opacity
:
0
.8
;
color
:
rgb
(
var
(
--
arcoblue-5
));
}
}
// Newsletter 副标题和协议文字使用动态文本色
.footer-subtitle
{
color
:
rgba
(
255
,
255
,
255
,
0
.7
);
}
.footer-agreement
{
color
:
rgba
(
255
,
255
,
255
,
0
.6
);
}
// 链接使用动态配色
.footer-link
{
color
:
rgba
(
255
,
255
,
255
,
0
.7
);
&
:hover
{
color
:
rgb
(
var
(
--
arcoblue-5
));
}
}
// 版权信息使用动态文本色
.footer-copyright
{
color
:
rgba
(
255
,
255
,
255
,
0
.7
);
}
// 返回顶部按钮使用动态配色
.footer-back-to-top
{
min-width
:
auto
;
padding
:
4px
8px
;
color
:
rgba
(
255
,
255
,
255
,
0
.7
)
!
important
;
&
:hover
{
color
:
rgb
(
var
(
--
arcoblue-5
))
!
important
;
background-color
:
rgba
(
var
(
--
arcoblue-6
)
,
0
.1
)
!
important
;
}
}
}
.payment-icon
{
.payment-icon
{
...
@@ -276,6 +327,7 @@ const scrollToTop = () => {
...
@@ -276,6 +327,7 @@ const scrollToTop = () => {
padding
:
4px
8px
;
padding
:
4px
8px
;
}
}
// 输入框使用动态配色
:deep
(
.arco-input
)
{
:deep
(
.arco-input
)
{
background-color
:
rgba
(
255
,
255
,
255
,
0
.1
);
background-color
:
rgba
(
255
,
255
,
255
,
0
.1
);
border-color
:
rgba
(
255
,
255
,
255
,
0
.2
);
border-color
:
rgba
(
255
,
255
,
255
,
0
.2
);
...
@@ -287,7 +339,18 @@ const scrollToTop = () => {
...
@@ -287,7 +339,18 @@ const scrollToTop = () => {
&
:focus
{
&
:focus
{
background-color
:
rgba
(
255
,
255
,
255
,
0
.15
);
background-color
:
rgba
(
255
,
255
,
255
,
0
.15
);
border-color
:
rgba
(
255
,
255
,
255
,
0
.3
);
border-color
:
rgb
(
var
(
--
arcoblue-5
));
}
}
// 按钮使用动态配色
:deep
(
.arco-btn-primary
)
{
background-color
:
rgb
(
var
(
--
arcoblue-6
));
border-color
:
rgb
(
var
(
--
arcoblue-6
));
&
:hover
{
background-color
:
rgb
(
var
(
--
arcoblue-5
));
border-color
:
rgb
(
var
(
--
arcoblue-5
));
}
}
}
}
</
style
>
</
style
>
...
...
src/layouts/components/HeaderDestinationMenu.vue
View file @
5e6bcbb9
<
template
>
<
template
>
<a-dropdown
trigger=
"
hover
"
position=
"bl"
>
<a-dropdown
trigger=
"
click
"
position=
"bl"
>
<a-button
type=
"text"
class=
"!px-2"
>
<a-button
type=
"text"
class=
"!px-2"
>
<v-icon
name=
"geolocation"
class=
"text-[16px]"
/>
<v-icon
name=
"geolocation"
class=
"text-[16px]"
/>
<span
class=
"ml-1"
>
{{
t
(
'header.destinations'
)
}}
</span>
<span
class=
"ml-1"
>
{{
t
(
'header.destinations'
)
}}
</span>
...
@@ -7,6 +7,9 @@
...
@@ -7,6 +7,9 @@
<template
#
content
>
<template
#
content
>
<div
class=
"destination-panel"
v-if=
"placeTree.length"
>
<div
class=
"destination-panel"
v-if=
"placeTree.length"
>
<div
class=
"continent-list"
>
<div
class=
"continent-list"
>
<div
class=
"continent-item"
:class=
"
{ active: -1 === activeIndex }" @mouseenter="activeIndex = -1">
{{
t
(
'header.recommond'
)
}}
</div>
<div
<div
v-for=
"(continent, index) in placeTree"
v-for=
"(continent, index) in placeTree"
:key=
"continent.value || index"
:key=
"continent.value || index"
...
@@ -17,20 +20,40 @@
...
@@ -17,20 +20,40 @@
{{
continent
.
label
}}
{{
continent
.
label
}}
</div>
</div>
</div>
</div>
<div
class=
"continent-content"
v-if=
"activeContinent"
>
<div
v-show=
"activeIndex === -1"
class=
"p-4 flex-1 min-w-0 overflow-y-auto"
>
<div
class=
"font-bold text-lg"
>
{{
t
(
'header.hotDestinations'
)
}}
</div>
<div
class=
"pt-4 mb-2"
v-for=
"(val,index) in hotDestinations"
:key=
"index"
>
<div
class=
"font-bold text-md mb-3"
>
{{
val
.
name
}}
</div>
<div
class=
"grid grid-cols-4 gap-4"
>
<div
class=
"rounded-md flex items-center justify-center cursor-pointer overlap relative overflow-hidden !aspect-[3/2] !object-cover"
v-for=
"(item,index) in val.reList"
:key=
"index"
>
<a-image
:src=
"item.placeDetail?.backgroundImage || ''"
show-loader
></a-image>
<div
class=
"absolute bottom-0 left-0 right-0 p-2"
>
<div
class=
"text-white text-sm font-bold"
>
{{
item
.
placeDetail
?.
name
}}
</div>
</div>
</div>
</div>
</div>
</div>
<div
class=
"continent-content"
v-if=
"activeContinent && activeIndex !== -1"
>
<template
v-for=
"country in activeContinent.children"
:key=
"country.value"
>
<template
v-for=
"country in activeContinent.children"
:key=
"country.value"
>
<div
class=
"country-block"
>
<div
class=
"country-block"
>
<div
class=
"country-title"
>
<div
class=
"country-title"
>
{{
country
.
label
}}
{{
country
.
label
}}
</div>
</div>
<div
class=
"place-grid"
>
<div
class=
"place-grid"
>
<span
class=
"place-item
text-gray-600 hover:bg-gray-200 hover:text-gray-800
rounded-md"
>
<span
class=
"place-item
destination-place-item
rounded-md"
>
{{
t
(
'place.all'
)
}}
{{
t
(
'place.all'
)
}}
</span>
</span>
<span
<span
v-for=
"place in country.children"
v-for=
"place in country.children"
:key=
"place.value"
:key=
"place.value"
class=
"place-item
text-gray-600 hover:bg-gray-200 hover:text-gray-800
rounded-md"
class=
"place-item
destination-place-item
rounded-md"
>
>
{{
place
.
label
}}
{{
place
.
label
}}
</span>
</span>
...
@@ -49,29 +72,42 @@
...
@@ -49,29 +72,42 @@
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
onMounted
,
ref
,
computed
}
from
'vue'
import
{
onMounted
,
ref
,
computed
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
import
{
useI18n
}
from
'vue-i18n'
import
PlaceService
from
'@/services/PlaceService'
import
PlaceService
,
{
type
PlaceGroupOutputDto
}
from
'@/services/PlaceService'
import
type
{
PlaceTreeNode
}
from
'@/types/place'
import
type
{
PlaceTreeNode
}
from
'@/types/place'
import
{
Message
}
from
'@arco-design/web-vue'
import
{
Message
}
from
'@arco-design/web-vue'
import
{
useSystemConfigStore
}
from
'@/stores'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
placeTree
=
ref
<
PlaceTreeNode
[]
>
([])
const
placeTree
=
ref
<
PlaceTreeNode
[]
>
([])
const
activeIndex
=
ref
(
0
)
const
activeIndex
=
ref
(
-
1
)
const
activeContinent
=
computed
(()
=>
placeTree
.
value
[
activeIndex
.
value
])
const
activeContinent
=
computed
(()
=>
placeTree
.
value
[
activeIndex
.
value
])
const
systemConfigStore
=
useSystemConfigStore
()
const
hotDestinations
=
ref
<
PlaceGroupOutputDto
[]
>
([])
const
loadPlaces
=
async
()
=>
{
const
loadPlaces
=
async
()
=>
{
try
{
try
{
const
data
=
await
PlaceService
.
getPlaceTreeAsync
()
const
data
=
await
PlaceService
.
getPlaceTreeAsync
()
placeTree
.
value
=
data
||
[]
placeTree
.
value
=
data
||
[]
activeIndex
.
value
=
0
activeIndex
.
value
=
-
1
console
.
log
(
data
)
console
.
log
(
data
)
}
catch
(
error
)
{
}
catch
(
error
)
{
Message
.
error
(
t
(
'header.destinationsFailed'
))
Message
.
error
(
t
(
'header.destinationsFailed'
))
}
}
}
}
const
loadHotDestinations
=
async
()
=>
{
try
{
const
data
=
await
PlaceService
.
getRecommendPlaceListAsync
(
systemConfigStore
.
distributorId
||
0
)
hotDestinations
.
value
=
data
||
[]
}
catch
(
error
)
{
Message
.
error
(
t
(
'header.hotDestinationsFailed'
))
}
}
onMounted
(()
=>
{
onMounted
(()
=>
{
loadPlaces
()
loadPlaces
()
loadHotDestinations
()
})
})
</
script
>
</
script
>
...
@@ -113,7 +149,7 @@ onMounted(() => {
...
@@ -113,7 +149,7 @@ onMounted(() => {
padding
:
15px
18px
;
padding
:
15px
18px
;
cursor
:
pointer
;
cursor
:
pointer
;
font-weight
:
500
;
font-weight
:
500
;
color
:
#4b5563
;
color
:
rgb
(
var
(
--
customColor-text-7
))
;
transition
:
all
.2s
ease
;
transition
:
all
.2s
ease
;
}
}
.continent-item.active
,
.continent-item.active
,
...
@@ -129,12 +165,12 @@ onMounted(() => {
...
@@ -129,12 +165,12 @@ onMounted(() => {
.country-block
+
.country-block
{
.country-block
+
.country-block
{
margin-top
:
18px
;
margin-top
:
18px
;
padding-top
:
18px
;
padding-top
:
18px
;
border-top
:
1px
solid
#f1f1f1
;
border-top
:
1px
solid
rgb
(
var
(
--
arcoblue-7
))
;
}
}
.country-title
{
.country-title
{
font-weight
:
600
;
font-weight
:
600
;
margin-bottom
:
12px
;
margin-bottom
:
12px
;
color
:
#111
;
color
:
rgb
(
var
(
--
customColor-text-10
))
;
}
}
.place-grid
{
.place-grid
{
display
:
grid
;
display
:
grid
;
...
@@ -146,6 +182,17 @@ onMounted(() => {
...
@@ -146,6 +182,17 @@ onMounted(() => {
cursor
:
pointer
;
cursor
:
pointer
;
padding
:
10px
10px
;
padding
:
10px
10px
;
}
}
// 目的地项使用动态配色
.destination-place-item
{
color
:
rgb
(
var
(
--
customColor-text-7
));
transition
:
all
0
.2s
ease
;
&
:hover
{
background-color
:
rgba
(
var
(
--
arcoblue-6
)
,
0
.1
);
color
:
rgb
(
var
(
--
arcoblue-6
));
}
}
.destination-empty
{
.destination-empty
{
width
:
240px
;
width
:
240px
;
height
:
120px
;
height
:
120px
;
...
...
src/layouts/components/HeaderNavBar.vue
View file @
5e6bcbb9
...
@@ -71,11 +71,45 @@ const handleMenuClick = (key: string) => {
...
@@ -71,11 +71,45 @@ const handleMenuClick = (key: string) => {
<
style
scoped
lang=
"scss"
>
<
style
scoped
lang=
"scss"
>
.web-header-menu
{
.web-header-menu
{
border-bottom
:
none
;
border-bottom
:
none
;
}
// :deep(.arco-menu-inner) {
// 导航菜单项使用动态配色
// padding: 10px 0 !important;
:deep
(
.arco-menu-item
)
{
// overflow: hidden;
color
:
rgb
(
var
(
--
customColor-text-10
));
// }
transition
:
all
0
.2s
ease
;
&
:hover
{
color
:
rgb
(
var
(
--
arcoblue-6
));
background-color
:
rgba
(
var
(
--
arcoblue-6
)
,
0
.05
);
}
&
.arco-menu-selected
{
color
:
rgb
(
var
(
--
arcoblue-6
));
background-color
:
rgba
(
var
(
--
arcoblue-6
)
,
0
.08
);
&
:
:
after
{
background-color
:
rgb
(
var
(
--
arcoblue-6
));
}
}
}
// 子菜单标题使用动态配色
:deep
(
.arco-sub-menu-title
)
{
color
:
rgb
(
var
(
--
customColor-text-10
));
transition
:
all
0
.2s
ease
;
&
:hover
{
color
:
rgb
(
var
(
--
arcoblue-6
));
background-color
:
rgba
(
var
(
--
arcoblue-6
)
,
0
.05
);
}
}
// 子菜单项使用动态配色
:deep
(
.arco-menu-item
)
{
&
.arco-menu-selected
{
color
:
rgb
(
var
(
--
arcoblue-6
));
background-color
:
rgba
(
var
(
--
arcoblue-6
)
,
0
.08
);
}
}
}
</
style
>
</
style
>
src/layouts/components/HeaderTopBar.vue
View file @
5e6bcbb9
...
@@ -192,11 +192,11 @@ const handleGoHome = () => {
...
@@ -192,11 +192,11 @@ const handleGoHome = () => {
font
-
size
:
13
px
;
font
-
size
:
13
px
;
}
}
.
currency
-
option
:
hover
{
.
currency
-
option
:
hover
{
background
:
rgba
(
74
,
144
,
226
,
0.1
);
background
:
rgba
(
var
(
--
arcoblue
-
6
)
,
0.1
);
color
:
rgb
(
var
(
--
arcoblue
-
6
));
color
:
rgb
(
var
(
--
arcoblue
-
6
));
}
}
.
currency
-
option
.
active
{
.
currency
-
option
.
active
{
background
:
rgba
(
74
,
144
,
226
,
0.15
);
background
:
rgba
(
var
(
--
arcoblue
-
6
)
,
0.15
);
color
:
rgb
(
var
(
--
arcoblue
-
6
));
color
:
rgb
(
var
(
--
arcoblue
-
6
));
font
-
weight
:
600
;
font
-
weight
:
600
;
}
}
...
...
src/layouts/components/Headers.vue
View file @
5e6bcbb9
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
<div
class=
"fixed top-0 left-0 right-0 z-10 shadow-sm customPrimary-bg-7 flex justify-center"
>
<div
class=
"fixed top-0 left-0 right-0 z-10 shadow-sm customPrimary-bg-7 flex justify-center"
>
<HeaderTopBar
/>
<HeaderTopBar
/>
</div>
</div>
<div
class=
"h
-[1px] w-full bg-gray-700/5
"
></div>
<div
class=
"h
eader-divider
"
></div>
<HeaderNavBar
/>
<HeaderNavBar
/>
</header>
</header>
</
template
>
</
template
>
...
@@ -13,8 +13,35 @@ import HeaderNavBar from './HeaderNavBar.vue'
...
@@ -13,8 +13,35 @@ import HeaderNavBar from './HeaderNavBar.vue'
</
script
>
</
script
>
<
style
scoped
lang=
"scss"
>
<
style
scoped
lang=
"scss"
>
:deep
(
.arco-btn-primary
:hover
,
.arco-btn-primary
)
{
// 确保所有按钮使用动态配色
:deep
(
.arco-btn-primary
)
{
background-color
:
rgb
(
var
(
--
arcoblue-6
));
background-color
:
rgb
(
var
(
--
arcoblue-6
));
border-color
:
rgb
(
var
(
--
arcoblue-6
));
&
:hover
{
background-color
:
rgb
(
var
(
--
arcoblue-5
));
border-color
:
rgb
(
var
(
--
arcoblue-5
));
}
&
:active
{
background-color
:
rgb
(
var
(
--
arcoblue-6
));
border-color
:
rgb
(
var
(
--
arcoblue-6
));
}
}
// 文本按钮的hover效果
:deep
(
.arco-btn-text
)
{
&
:hover
{
background-color
:
rgba
(
var
(
--
arcoblue-6
)
,
0
.08
);
color
:
rgb
(
var
(
--
arcoblue-6
));
}
}
// 分隔线使用动态配色
.header-divider
{
height
:
1px
;
width
:
100%
;
background-color
:
rgba
(
var
(
--
arcoblue-8
)
,
0
.05
);
}
}
</
style
>
</
style
>
src/services/PlaceService.ts
View file @
5e6bcbb9
...
@@ -60,6 +60,42 @@ export interface PlaceOutputDto extends PlaceInputDto {
...
@@ -60,6 +60,42 @@ export interface PlaceOutputDto extends PlaceInputDto {
id
:
string
id
:
string
}
}
/**
* 目的地推荐DTO
*/
export
interface
PlaceReDto
{
/** 目的地名称 */
name
?:
string
|
null
/** 背景图片 */
backgroundImage
?:
string
|
null
/** 状态(true=启用,false=禁用) */
state
?:
boolean
|
null
}
/**
* 分组目的地关联DTO
*/
export
interface
GroupReDto
{
/** 目的地ID */
placeId
:
string
/** 目的地详情 */
placeDetail
:
PlaceReDto
}
/**
* 目的地分组输出DTO
*/
export
interface
PlaceGroupOutputDto
{
/** 分组ID */
id
:
string
/** 推荐分组名称 */
name
?:
string
|
null
/** 排序 */
orderNum
:
number
/** 目的地列表 */
reList
?:
GroupReDto
[]
|
null
}
class
PlaceService
{
class
PlaceService
{
static
async
getPlaceTreeAsync
():
Promise
<
PlaceTreeNode
[]
>
{
static
async
getPlaceTreeAsync
():
Promise
<
PlaceTreeNode
[]
>
{
const
response
=
await
OtaRequest
.
get
(
'/sys-management/place-services/tree'
)
const
response
=
await
OtaRequest
.
get
(
'/sys-management/place-services/tree'
)
...
@@ -71,6 +107,20 @@ class PlaceService {
...
@@ -71,6 +107,20 @@ class PlaceService {
const
response
=
await
OtaRequest
.
get
(
'/sys-management/place-services/paged'
,
params
)
const
response
=
await
OtaRequest
.
get
(
'/sys-management/place-services/paged'
,
params
)
return
response
as
unknown
as
PagedResult
<
PlaceOutputDto
>
return
response
as
unknown
as
PagedResult
<
PlaceOutputDto
>
}
}
/**
* 获取推荐目的地列表
* @param distributorId 分销商ID
* @returns 推荐目的地分组列表
*/
static
async
getRecommendPlaceListAsync
(
distributorId
:
number
):
Promise
<
PlaceGroupOutputDto
[]
>
{
const
response
=
await
OtaRequest
.
get
(
`/sys-management/place-services/recommend-place-list/
${
distributorId
}
`
)
return
response
as
unknown
as
PlaceGroupOutputDto
[]
}
}
}
export
default
PlaceService
export
default
PlaceService
...
...
src/utils/themeUtils.ts
0 → 100644
View file @
5e6bcbb9
/**
* 主题配色工具类
* 支持从后端配置动态加载和应用配色方案
*/
export
interface
ThemeColors
{
// 主色系(基于webSiteColor生成)
primary
:
{
[
key
:
number
]:
string
1
:
string
// 最深
2
:
string
3
:
string
4
:
string
5
:
string
// hover色
6
:
string
// 主色
7
:
string
// 浅色背景
8
:
string
// 深色文字
9
:
string
// 强调色(如橙色)
10
:
string
// 最浅背景
11
:
string
// 极浅背景
}
// 文本色系
text
:
{
[
key
:
number
]:
string
10
:
string
// 最深
9
:
string
8
:
string
7
:
string
6
:
string
5
:
string
4
:
string
}
// 中性色(灰色系)
gray
:
{
[
key
:
number
]:
string
10
:
string
9
:
string
8
:
string
7
:
string
6
:
string
5
:
string
4
:
string
}
}
/**
* 默认配色方案(绿色系,兼容现有设计)
*/
const
DEFAULT_THEME
:
ThemeColors
=
{
primary
:
{
1
:
'#48605B'
,
2
:
'#EBEBE9'
,
3
:
'#8AA88A'
,
4
:
'#C0CEB3'
,
5
:
'#95A997'
,
6
:
'#4A664D'
,
7
:
'#e3e6da'
,
8
:
'#263628'
,
9
:
'#FF9707'
,
10
:
'#F5F6F0'
,
11
:
'#F0F1EB'
,
},
text
:
{
10
:
'#0c150d'
,
9
:
'#5a5a5a'
,
8
:
'#133537'
,
7
:
'#606961'
,
6
:
'#A3A4A0'
,
5
:
'#4A664D'
,
4
:
'#8EAD8E'
,
},
gray
:
{
10
:
'#0c150d'
,
9
:
'#5a5a5a'
,
8
:
'#133537'
,
7
:
'#606961'
,
6
:
'#A3A4A0'
,
5
:
'#4A664D'
,
4
:
'#8EAD8E'
,
},
}
/**
* 将十六进制颜色转换为RGB值(用于CSS变量)
*/
function
hexToRgb
(
hex
:
string
):
string
{
const
result
=
/^#
?([
a-f
\d]{2})([
a-f
\d]{2})([
a-f
\d]{2})
$/i
.
exec
(
hex
)
if
(
!
result
)
return
'0, 0, 0'
const
r
=
parseInt
(
result
[
1
],
16
)
const
g
=
parseInt
(
result
[
2
],
16
)
const
b
=
parseInt
(
result
[
3
],
16
)
return
`
${
r
}
,
${
g
}
,
${
b
}
`
}
/**
* 根据主色生成配色方案
* @param primaryColor 主色(十六进制,如 #4A664D)
* @returns 完整的配色方案
*/
export
function
generateThemeFromPrimary
(
primaryColor
:
string
):
ThemeColors
{
// 如果主色无效,使用默认配色
if
(
!
primaryColor
||
!
/^#
([
A-Fa-f0-9
]{6}
|
[
A-Fa-f0-9
]{3})
$/
.
test
(
primaryColor
))
{
return
DEFAULT_THEME
}
// 解析主色RGB
const
rgb
=
hexToRgb
(
primaryColor
).
split
(
', '
).
map
(
Number
)
const
[
r
,
g
,
b
]
=
rgb
// 生成色阶(基于主色进行明暗变化)
const
generateShade
=
(
ratio
:
number
):
string
=>
{
const
newR
=
Math
.
max
(
0
,
Math
.
min
(
255
,
Math
.
round
(
r
*
ratio
)))
const
newG
=
Math
.
max
(
0
,
Math
.
min
(
255
,
Math
.
round
(
g
*
ratio
)))
const
newB
=
Math
.
max
(
0
,
Math
.
min
(
255
,
Math
.
round
(
b
*
ratio
)))
return
`#
${[
newR
,
newG
,
newB
].
map
(
x
=>
x
.
toString
(
16
).
padStart
(
2
,
'0'
)).
join
(
''
)}
`
}
const
generateTint
=
(
ratio
:
number
):
string
=>
{
const
newR
=
Math
.
max
(
0
,
Math
.
min
(
255
,
Math
.
round
(
r
+
(
255
-
r
)
*
ratio
)))
const
newG
=
Math
.
max
(
0
,
Math
.
min
(
255
,
Math
.
round
(
g
+
(
255
-
g
)
*
ratio
)))
const
newB
=
Math
.
max
(
0
,
Math
.
min
(
255
,
Math
.
round
(
b
+
(
255
-
b
)
*
ratio
)))
return
`#
${[
newR
,
newG
,
newB
].
map
(
x
=>
x
.
toString
(
16
).
padStart
(
2
,
'0'
)).
join
(
''
)}
`
}
// 生成色阶
return
{
primary
:
{
1
:
generateShade
(
0.6
),
// 最深
2
:
generateTint
(
0.92
),
// 极浅
3
:
generateTint
(
0.5
),
// 中等浅
4
:
generateTint
(
0.7
),
// 浅
5
:
generateTint
(
0.3
),
// hover色(稍浅)
6
:
primaryColor
,
// 主色
7
:
generateTint
(
0.88
),
// 浅色背景
8
:
generateShade
(
0.3
),
// 深色文字
9
:
DEFAULT_THEME
.
primary
[
9
],
// 强调色保持橙色
10
:
generateTint
(
0.95
),
// 最浅背景
11
:
generateTint
(
0.97
),
// 极浅背景
},
text
:
{
10
:
generateShade
(
0.1
),
// 最深文字
9
:
generateShade
(
0.35
),
// 深灰
8
:
generateShade
(
0.25
),
// 中深灰
7
:
generateShade
(
0.4
),
// 中灰
6
:
generateTint
(
0.35
),
// 浅灰
5
:
primaryColor
,
// 主色文字
4
:
generateTint
(
0.45
),
// 浅色文字
},
gray
:
DEFAULT_THEME
.
gray
,
// 灰色系保持默认
}
}
/**
* 应用配色方案到CSS变量
* @param theme 配色方案
*/
export
function
applyTheme
(
theme
:
ThemeColors
):
void
{
const
root
=
document
.
documentElement
const
body
=
document
.
body
// 应用主色系(Arco Design兼容)
body
.
style
.
setProperty
(
'--arcoblue-11'
,
hexToRgb
(
theme
.
primary
[
11
]))
body
.
style
.
setProperty
(
'--arcoblue-10'
,
hexToRgb
(
theme
.
primary
[
10
]))
body
.
style
.
setProperty
(
'--arcoblue-9'
,
hexToRgb
(
theme
.
primary
[
9
]))
body
.
style
.
setProperty
(
'--arcoblue-8'
,
hexToRgb
(
theme
.
primary
[
8
]))
body
.
style
.
setProperty
(
'--arcoblue-7'
,
hexToRgb
(
theme
.
primary
[
7
]))
body
.
style
.
setProperty
(
'--arcoblue-6'
,
hexToRgb
(
theme
.
primary
[
6
]))
body
.
style
.
setProperty
(
'--arcoblue-5'
,
hexToRgb
(
theme
.
primary
[
5
]))
body
.
style
.
setProperty
(
'--arcoblue-4'
,
hexToRgb
(
theme
.
primary
[
4
]))
body
.
style
.
setProperty
(
'--arcoblue-3'
,
hexToRgb
(
theme
.
primary
[
3
]))
body
.
style
.
setProperty
(
'--arcoblue-2'
,
hexToRgb
(
theme
.
primary
[
2
]))
body
.
style
.
setProperty
(
'--arcoblue-1'
,
hexToRgb
(
theme
.
primary
[
1
]))
// 应用自定义主色系
root
.
style
.
setProperty
(
'--customPrimary-11'
,
theme
.
primary
[
11
])
root
.
style
.
setProperty
(
'--customPrimary-10'
,
theme
.
primary
[
10
])
root
.
style
.
setProperty
(
'--customPrimary-9'
,
theme
.
primary
[
9
])
root
.
style
.
setProperty
(
'--customPrimary-8'
,
theme
.
primary
[
8
])
root
.
style
.
setProperty
(
'--customPrimary-7'
,
theme
.
primary
[
7
])
root
.
style
.
setProperty
(
'--customPrimary-6'
,
theme
.
primary
[
6
])
root
.
style
.
setProperty
(
'--customPrimary-5'
,
theme
.
primary
[
5
])
root
.
style
.
setProperty
(
'--customPrimary-4'
,
theme
.
primary
[
4
])
root
.
style
.
setProperty
(
'--customPrimary-3'
,
theme
.
primary
[
3
])
root
.
style
.
setProperty
(
'--customPrimary-2'
,
theme
.
primary
[
2
])
root
.
style
.
setProperty
(
'--customPrimary-1'
,
theme
.
primary
[
1
])
// 应用文本色系
root
.
style
.
setProperty
(
'--customColor-text-10'
,
theme
.
text
[
10
])
root
.
style
.
setProperty
(
'--customColor-text-9'
,
theme
.
text
[
9
])
root
.
style
.
setProperty
(
'--customColor-text-8'
,
theme
.
text
[
8
])
root
.
style
.
setProperty
(
'--customColor-text-7'
,
theme
.
text
[
7
])
root
.
style
.
setProperty
(
'--customColor-text-6'
,
theme
.
text
[
6
])
root
.
style
.
setProperty
(
'--customColor-text-5'
,
theme
.
text
[
5
])
root
.
style
.
setProperty
(
'--customColor-text-4'
,
theme
.
text
[
4
])
// 应用灰色系
body
.
style
.
setProperty
(
'--gray-10'
,
hexToRgb
(
theme
.
gray
[
10
]))
body
.
style
.
setProperty
(
'--gray-9'
,
hexToRgb
(
theme
.
gray
[
9
]))
body
.
style
.
setProperty
(
'--gray-8'
,
hexToRgb
(
theme
.
gray
[
8
]))
body
.
style
.
setProperty
(
'--gray-7'
,
hexToRgb
(
theme
.
gray
[
7
]))
body
.
style
.
setProperty
(
'--gray-6'
,
hexToRgb
(
theme
.
gray
[
6
]))
body
.
style
.
setProperty
(
'--gray-5'
,
hexToRgb
(
theme
.
gray
[
5
]))
body
.
style
.
setProperty
(
'--gray-4'
,
hexToRgb
(
theme
.
gray
[
4
]))
}
/**
* 从后端配置加载并应用配色
* @param webSiteColor 网站主色(十六进制)
*/
export
function
loadThemeFromConfig
(
webSiteColor
?:
string
|
null
):
void
{
if
(
webSiteColor
)
{
const
theme
=
generateThemeFromPrimary
(
webSiteColor
)
applyTheme
(
theme
)
}
else
{
// 使用默认配色
applyTheme
(
DEFAULT_THEME
)
}
}
/**
* 重置为默认配色
*/
export
function
resetToDefaultTheme
():
void
{
applyTheme
(
DEFAULT_THEME
)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment