Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
S
SuperMan
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
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
罗超
SuperMan
Commits
6d033a4b
Commit
6d033a4b
authored
Oct 09, 2025
by
黄奎
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
http://gitlab.oytour.com/luochao/superman
parents
68d5c6bf
c8d47ffc
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1972 additions
and
4 deletions
+1972
-4
BaseListManager.css
src/components/common/BaseListManager.css
+3
-0
QuoteDrawer.vue
src/pages/quoted-price/components/QuoteDrawer.vue
+1059
-0
create.vue
src/pages/quoted-price/create.vue
+167
-0
edit.vue
src/pages/quoted-price/edit.vue
+172
-0
price.vue
src/pages/quoted-price/price.vue
+443
-0
index.js
src/plug/index.js
+4
-4
config.js
src/router/config.js
+24
-0
quote.js
src/services/quote.js
+100
-0
No files found.
src/components/common/BaseListManager.css
View file @
6d033a4b
...
...
@@ -828,4 +828,7 @@
line-height
:
14px
;
min-width
:
16px
;
padding
:
0
4px
;
}
.el-drawer__header
{
margin-bottom
:
20px
!important
;
}
\ No newline at end of file
src/pages/quoted-price/components/QuoteDrawer.vue
0 → 100644
View file @
6d033a4b
<
template
>
<el-drawer
:visible
.
sync=
"drawerVisible"
:title=
"isEdit ? '编辑报价单' : '新建报价单'"
:size=
"drawerWidth"
:before-close=
"handleClose"
class=
"quote-drawer"
>
<div
class=
"drawer-content"
v-loading=
"loading"
>
<!-- 基本信息 -->
<div
class=
"section"
style=
"padding: 0 20px;"
>
<!--
<div
class=
"section-header"
>
<h3>
基本信息
</h3>
</div>
-->
<el-form
:model=
"form"
ref=
"form"
label-width=
"70px"
size=
"small"
>
<el-row
:gutter=
"20"
>
<!--
<el-col
:span=
"12"
>
<el-form-item
label=
"报价单名称"
prop=
"quoteName"
required
>
<el-input
v-model=
"form.quoteName"
placeholder=
"请输入报价单名称"
/>
</el-form-item>
</el-col>
-->
<el-col
:span=
"8"
>
<el-form-item
label=
"行程名称"
>
<span>
{{
itineraryInfo
.
title
||
'未选择行程'
}}
</span>
</el-form-item>
</el-col>
<el-col
:span=
"8"
>
<el-form-item
label=
"有效期"
prop=
"validDate"
>
<el-date-picker
v-model=
"form.validDate"
type=
"date"
placeholder=
"选择有效期"
value-format=
"yyyy-MM-dd"
style=
"width: 100%"
/>
</el-form-item>
</el-col>
<el-col
:span=
"8"
>
<div
style=
"text-align: right;"
>
<span>
报价总金额:
</span>
<span
class=
"total-price"
>
¥
{{
formatPrice
(
totalAmount
)
}}
</span>
</div>
</el-col>
</el-row>
</el-form>
</div>
<!-- 报价项目 -->
<div
class=
"section"
style=
"flex: 1;height: 1px; display: flex;flex-direction: column;"
>
<div
class=
"section-header"
>
<h3>
报价项目
</h3>
<el-dropdown
@
command=
"handleAddCategory"
trigger=
"click"
>
<el-button
type=
"primary"
size=
"small"
>
添加大类
<i
class=
"el-icon-arrow-down el-icon--right"
></i>
</el-button>
<el-dropdown-menu
slot=
"dropdown"
>
<el-dropdown-item
v-for=
"category in availableCategories"
:key=
"category.key"
:command=
"category.key"
>
{{
category
.
name
}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<!-- 报价项目列表 -->
<div
v-if=
"form.categories.length > 0"
class=
"quote-categories"
>
<div
v-for=
"category in form.categories"
:key=
"category.key"
class=
"category-section"
>
<div
class=
"category-header"
@
click=
"toggleCategoryCollapse(category.key)"
>
<div
class=
"category-title"
>
<i
:class=
"categoryCollapsed[category.key] ? 'el-icon-arrow-right' : 'el-icon-arrow-down'"
class=
"collapse-icon"
></i>
<i
:class=
"getCategoryIcon(category.key)"
></i>
<span>
{{
category
.
name
}}
</span>
<span
class=
"category-total"
>
合计: ¥
{{
formatPrice
(
getCategoryTotal
(
category
))
}}
</span>
</div>
<div
class=
"category-actions"
@
click
.
stop
>
<el-button
type=
"text"
size=
"mini"
@
click=
"addCategoryItem(category)"
icon=
"el-icon-plus"
>
添加项目
</el-button>
<el-button
type=
"text"
size=
"mini"
@
click=
"removeCategory(category.key)"
icon=
"el-icon-delete"
class=
"danger-btn"
>
删除大类
</el-button>
</div>
</div>
<!-- 项目列表 - 聚合布局 -->
<div
class=
"category-items"
v-show=
"!categoryCollapsed[category.key]"
>
<!-- 按项目名称聚合显示 -->
<div
v-for=
"(groupedItem, groupIndex) in getGroupedItems(category)"
:key=
"groupIndex"
class=
"grouped-item"
>
<!-- 项目组头部 -->
<div
class=
"item-group-header"
>
<div
class=
"group-info"
>
<el-input
v-model=
"groupedItem.period"
placeholder=
"时间"
size=
"mini"
style=
"width: 80px; margin-right: 8px;"
@
input=
"updateGroupPeriod(category, groupedItem.name, $event)"
/>
<el-input
v-model=
"groupedItem.name"
placeholder=
"项目名称"
size=
"mini"
style=
"width: 200px; margin-right: 8px;"
@
input=
"updateGroupName(category, groupedItem.originalName, $event)"
/>
<span
class=
"group-total"
>
小计: ¥
{{
formatPrice
(
groupedItem
.
total
)
}}
</span>
</div>
<div
class=
"group-actions"
>
<el-button
type=
"text"
size=
"mini"
@
click=
"addSpecificationToGroup(category, groupedItem.name)"
icon=
"el-icon-plus"
>
添加规格
</el-button>
<el-button
type=
"text"
size=
"mini"
@
click=
"removeItemGroup(category, groupedItem.name)"
icon=
"el-icon-delete"
class=
"danger-btn"
>
删除项目
</el-button>
</div>
</div>
<!-- 规格列表 -->
<div
class=
"specifications-list"
>
<div
v-for=
"(spec, specIndex) in groupedItem.specifications"
:key=
"specIndex"
class=
"specification-row"
>
<div
class=
"spec-info"
>
<el-input
v-model=
"spec.specification"
placeholder=
"规格说明(如:标准间、成人票等)"
size=
"mini"
style=
"width: 180px; margin-right: 8px;"
/>
</div>
<div
class=
"spec-price"
>
<el-input-number
v-model=
"spec.unitPrice"
:precision=
"2"
:step=
"1"
:min=
"0"
size=
"mini"
placeholder=
"单价"
style=
"width: 100px; margin-right: 8px;"
@
change=
"calculateItemTotal(spec)"
/>
<el-input-number
v-model=
"spec.quantity"
:precision=
"0"
:step=
"1"
:min=
"1"
size=
"mini"
placeholder=
"数量"
style=
"width: 80px; margin-right: 8px;"
@
change=
"calculateItemTotal(spec)"
/>
<span
class=
"spec-total"
>
¥
{{
formatPrice
(
spec
.
totalPrice
||
0
)
}}
</span>
</div>
<div
class=
"spec-actions"
>
<el-button
type=
"text"
size=
"mini"
@
click=
"removeSpecification(category, groupedItem.name, specIndex)"
icon=
"el-icon-delete"
class=
"danger-btn"
/>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div
v-if=
"category.items.length === 0"
class=
"empty-items"
>
<span>
暂无项目,点击"添加项目"开始添加
</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div
v-if=
"form.categories.length === 0"
class=
"empty-categories"
>
<el-empty
description=
"暂无报价项目,请添加大类开始创建报价单"
/>
</div>
</div>
<div
class=
"drawer-footer"
>
<el-button
@
click=
"handleClose"
>
取消
</el-button>
<el-button
type=
"primary"
@
click=
"handleSave"
:loading=
"saving"
>
{{
isEdit
?
'保存修改'
:
'创建报价单'
}}
</el-button>
</div>
</div>
<!-- 底部操作栏 -->
</el-drawer>
</
template
>
<
script
>
import
{
quoteService
}
from
'@/services/quote'
import
{
itineraryService
}
from
'@/services/itinerary'
export
default
{
name
:
'QuoteDrawer'
,
props
:
{
visible
:
{
type
:
Boolean
,
default
:
false
},
quoteId
:
{
type
:
String
,
default
:
null
},
itineraryId
:
{
type
:
String
,
default
:
null
}
},
data
()
{
return
{
loading
:
false
,
saving
:
false
,
form
:
{
quoteName
:
''
,
validDate
:
''
,
categories
:
[]
},
itineraryInfo
:
{},
// 折叠状态管理
categoryCollapsed
:
{},
// 所有可用的大类
allCategories
:
[
{
key
:
'transport'
,
name
:
'大交通'
,
icon
:
'el-icon-truck'
},
{
key
:
'hotel'
,
name
:
'酒店住宿'
,
icon
:
'el-icon-house'
},
{
key
:
'meal'
,
name
:
'餐饮'
,
icon
:
'el-icon-food'
},
{
key
:
'ticket'
,
name
:
'票券'
,
icon
:
'el-icon-tickets'
},
{
key
:
'localTransport'
,
name
:
'交通'
,
icon
:
'el-icon-position'
},
{
key
:
'other'
,
name
:
'其它'
,
icon
:
'el-icon-more'
}
],
// 需要过滤的餐厅关键词
excludedMealKeywords
:
[
'酒店自助'
,
'酒店内享用'
,
'自理'
,
'敬请自理'
,
'客人自理'
,
'酒店早餐'
,
'含早'
,
'不含'
,
'无'
]
}
},
computed
:
{
drawerVisible
:
{
get
()
{
return
this
.
visible
},
set
(
val
)
{
this
.
$emit
(
'update:visible'
,
val
)
}
},
isEdit
()
{
return
!!
this
.
quoteId
},
drawerWidth
()
{
const
vw
=
Math
.
max
(
document
.
documentElement
.
clientWidth
||
0
,
window
.
innerWidth
||
0
)
const
calculatedWidth
=
Math
.
max
(
vw
*
0.8
,
1200
)
return
`
${
calculatedWidth
}
px`
},
availableCategories
()
{
const
existingKeys
=
this
.
form
.
categories
.
map
(
c
=>
c
.
key
)
return
this
.
allCategories
.
filter
(
c
=>
!
existingKeys
.
includes
(
c
.
key
))
},
totalAmount
()
{
return
this
.
form
.
categories
.
reduce
((
total
,
category
)
=>
{
return
total
+
this
.
getCategoryTotal
(
category
)
},
0
)
}
},
watch
:
{
visible
(
val
)
{
if
(
val
)
{
this
.
initDrawer
()
}
else
{
this
.
resetForm
()
}
}
},
methods
:
{
async
initDrawer
()
{
this
.
loading
=
true
try
{
if
(
this
.
isEdit
)
{
// 编辑模式:加载现有报价单
await
this
.
loadQuoteDetail
()
}
else
if
(
this
.
itineraryId
)
{
// 新增模式:根据行程自动生成
await
this
.
loadItineraryAndGenerate
()
}
else
{
// 纯新增模式
this
.
resetForm
()
}
}
catch
(
error
)
{
this
.
$message
.
error
(
'加载数据失败'
)
}
finally
{
this
.
loading
=
false
}
},
async
loadQuoteDetail
()
{
const
response
=
await
quoteService
.
getQuoteDetail
(
this
.
quoteId
)
if
(
response
&&
response
.
data
)
{
this
.
form
=
{
...
response
.
data
}
this
.
itineraryInfo
=
response
.
data
.
itinerary
||
{}
}
},
async
loadItineraryAndGenerate
()
{
// 加载行程信息
const
response
=
await
itineraryService
.
getItineraryDetail
(
this
.
itineraryId
)
if
(
response
)
{
this
.
itineraryInfo
=
response
this
.
form
.
quoteName
=
`
${
response
.
title
}
- 报价单`
// 根据行程自动生成报价项目
this
.
generateQuoteFromItinerary
(
response
)
}
},
generateQuoteFromItinerary
(
itinerary
)
{
const
categories
=
[]
// 生成大交通(支持不同票种)
if
(
itinerary
.
days
&&
itinerary
.
days
.
some
(
day
=>
day
.
traffics
&&
day
.
traffics
.
some
(
t
=>
t
.
type
===
'航班'
)
))
{
const
transportCategory
=
{
key
:
'transport'
,
name
:
'大交通'
,
items
:
[]
}
itinerary
.
days
.
forEach
((
day
,
dayIndex
)
=>
{
if
(
day
.
traffics
)
{
day
.
traffics
.
forEach
(
traffic
=>
{
if
(
traffic
.
type
===
'航班'
)
{
const
flightName
=
`
${
traffic
.
departure
}
-
${
traffic
.
destination
}
`
// 添加成人票
transportCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
`
,
name
:
flightName
,
specification
:
'成人票'
,
unitPrice
:
0
,
quantity
:
2
,
totalPrice
:
0
})
// 添加儿童票
transportCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
`
,
name
:
flightName
,
specification
:
'儿童票'
,
unitPrice
:
0
,
quantity
:
0
,
totalPrice
:
0
})
// 添加婴儿票
transportCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
`
,
name
:
flightName
,
specification
:
'婴儿票'
,
unitPrice
:
0
,
quantity
:
0
,
totalPrice
:
0
})
}
})
}
})
if
(
transportCategory
.
items
.
length
>
0
)
{
categories
.
push
(
transportCategory
)
}
}
// 生成酒店住宿(支持不同房型)
if
(
itinerary
.
days
&&
itinerary
.
days
.
some
(
day
=>
day
.
hotels
&&
day
.
hotels
.
length
>
0
))
{
const
hotelCategory
=
{
key
:
'hotel'
,
name
:
'酒店住宿'
,
items
:
[]
}
itinerary
.
days
.
forEach
((
day
,
dayIndex
)
=>
{
if
(
day
.
hotels
&&
day
.
hotels
.
length
>
0
)
{
day
.
hotels
.
forEach
(
hotel
=>
{
// 添加标准间
hotelCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
- D
${
dayIndex
+
2
}
`
,
name
:
hotel
.
name
,
specification
:
'标准间'
,
unitPrice
:
0
,
quantity
:
1
,
totalPrice
:
0
})
// 添加豪华间(默认数量为0)
hotelCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
- D
${
dayIndex
+
2
}
`
,
name
:
hotel
.
name
,
specification
:
'豪华间'
,
unitPrice
:
0
,
quantity
:
0
,
totalPrice
:
0
})
})
}
})
if
(
hotelCategory
.
items
.
length
>
0
)
{
categories
.
push
(
hotelCategory
)
}
}
// 生成餐饮(过滤无效餐厅信息)
if
(
itinerary
.
days
&&
itinerary
.
days
.
some
(
day
=>
day
.
caterings
&&
day
.
caterings
.
length
>
0
))
{
const
mealCategory
=
{
key
:
'meal'
,
name
:
'餐饮'
,
items
:
[]
}
itinerary
.
days
.
forEach
((
day
,
dayIndex
)
=>
{
if
(
day
.
caterings
&&
day
.
caterings
.
length
>
0
)
{
day
.
caterings
.
forEach
(
catering
=>
{
// 过滤无效的餐厅信息
if
(
this
.
isValidMeal
(
catering
.
name
))
{
mealCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
`
,
name
:
catering
.
name
,
specification
:
catering
.
type
,
unitPrice
:
0
,
quantity
:
2
,
totalPrice
:
0
})
}
})
}
})
if
(
mealCategory
.
items
.
length
>
0
)
{
categories
.
push
(
mealCategory
)
}
}
// 生成票券(支持不同票种)
if
(
itinerary
.
days
&&
itinerary
.
days
.
some
(
day
=>
day
.
attractions
&&
day
.
attractions
.
length
>
0
))
{
const
ticketCategory
=
{
key
:
'ticket'
,
name
:
'票券'
,
items
:
[]
}
itinerary
.
days
.
forEach
((
day
,
dayIndex
)
=>
{
if
(
day
.
attractions
&&
day
.
attractions
.
length
>
0
)
{
day
.
attractions
.
forEach
(
attraction
=>
{
// 添加成人票
ticketCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
`
,
name
:
attraction
.
name
,
specification
:
'成人票'
,
unitPrice
:
0
,
quantity
:
2
,
totalPrice
:
0
})
// 添加儿童票
ticketCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
`
,
name
:
attraction
.
name
,
specification
:
'儿童票'
,
unitPrice
:
0
,
quantity
:
0
,
totalPrice
:
0
})
// 添加老人票
ticketCategory
.
items
.
push
({
period
:
`D
${
dayIndex
+
1
}
`
,
name
:
attraction
.
name
,
specification
:
'老人票'
,
unitPrice
:
0
,
quantity
:
0
,
totalPrice
:
0
})
})
}
})
if
(
ticketCategory
.
items
.
length
>
0
)
{
categories
.
push
(
ticketCategory
)
}
}
// 生成地面交通
if
(
itinerary
.
days
&&
itinerary
.
days
.
some
(
day
=>
day
.
traffics
&&
day
.
traffics
.
some
(
t
=>
t
.
type
!==
'航班'
)
))
{
const
localTransportCategory
=
{
key
:
'localTransport'
,
name
:
'交通'
,
items
:
[]
}
// 添加接机
localTransportCategory
.
items
.
push
({
period
:
'D1'
,
name
:
'接机'
,
specification
:
'专车接机'
,
unitPrice
:
0
,
quantity
:
1
,
totalPrice
:
0
})
// 添加送机
localTransportCategory
.
items
.
push
({
period
:
`D
${
itinerary
.
days
.
length
}
`
,
name
:
'送机'
,
specification
:
'专车送机'
,
unitPrice
:
0
,
quantity
:
1
,
totalPrice
:
0
})
// 添加包车
if
(
itinerary
.
days
.
length
>
1
)
{
localTransportCategory
.
items
.
push
({
period
:
`D2-D
${
itinerary
.
days
.
length
-
1
}
`
,
name
:
'目的地包车'
,
specification
:
'全程包车'
,
unitPrice
:
0
,
quantity
:
itinerary
.
days
.
length
-
2
,
totalPrice
:
0
})
}
categories
.
push
(
localTransportCategory
)
}
this
.
form
.
categories
=
categories
},
// 验证餐厅信息是否有效
isValidMeal
(
mealName
)
{
if
(
!
mealName
)
{
return
false
}
const
trimmedName
=
mealName
.
trim
()
if
(
!
trimmedName
)
{
return
false
}
// 检查是否包含需要过滤的关键词
return
!
this
.
excludedMealKeywords
.
some
(
keyword
=>
trimmedName
.
includes
(
keyword
)
)
},
// 切换分类折叠状态
toggleCategoryCollapse
(
categoryKey
)
{
this
.
$set
(
this
.
categoryCollapsed
,
categoryKey
,
!
this
.
categoryCollapsed
[
categoryKey
])
},
handleAddCategory
(
categoryKey
)
{
const
category
=
this
.
allCategories
.
find
(
c
=>
c
.
key
===
categoryKey
)
if
(
category
)
{
this
.
form
.
categories
.
push
({
key
:
category
.
key
,
name
:
category
.
name
,
items
:
[]
})
// 新添加的分类默认展开
this
.
$set
(
this
.
categoryCollapsed
,
category
.
key
,
false
)
}
},
removeCategory
(
categoryKey
)
{
this
.
form
.
categories
=
this
.
form
.
categories
.
filter
(
c
=>
c
.
key
!==
categoryKey
)
},
// 获取按项目名称聚合的数据
getGroupedItems
(
category
)
{
const
grouped
=
{}
category
.
items
.
forEach
(
item
=>
{
const
key
=
item
.
name
||
'未命名项目'
if
(
!
grouped
[
key
])
{
grouped
[
key
]
=
{
name
:
key
,
originalName
:
key
,
period
:
item
.
period
,
specifications
:
[],
total
:
0
}
}
grouped
[
key
].
specifications
.
push
(
item
)
grouped
[
key
].
total
+=
(
item
.
totalPrice
||
0
)
})
return
Object
.
values
(
grouped
)
},
addCategoryItem
(
category
)
{
category
.
items
.
push
({
period
:
''
,
name
:
''
,
specification
:
''
,
unitPrice
:
0
,
quantity
:
1
,
totalPrice
:
0
})
},
// 为项目组添加新规格
addSpecificationToGroup
(
category
,
groupName
)
{
const
existingItem
=
category
.
items
.
find
(
item
=>
item
.
name
===
groupName
)
const
period
=
existingItem
?
existingItem
.
period
:
''
category
.
items
.
push
({
period
:
period
,
name
:
groupName
,
specification
:
''
,
unitPrice
:
0
,
quantity
:
1
,
totalPrice
:
0
})
},
// 删除整个项目组
removeItemGroup
(
category
,
groupName
)
{
category
.
items
=
category
.
items
.
filter
(
item
=>
item
.
name
!==
groupName
)
},
// 删除特定规格
removeSpecification
(
category
,
groupName
,
specIndex
)
{
const
groupItems
=
category
.
items
.
filter
(
item
=>
item
.
name
===
groupName
)
if
(
groupItems
.
length
>
specIndex
)
{
const
itemIndex
=
category
.
items
.
indexOf
(
groupItems
[
specIndex
])
if
(
itemIndex
>
-
1
)
{
category
.
items
.
splice
(
itemIndex
,
1
)
}
}
},
// 更新项目组名称
updateGroupName
(
category
,
originalName
,
newName
)
{
category
.
items
.
forEach
(
item
=>
{
if
(
item
.
name
===
originalName
)
{
item
.
name
=
newName
}
})
},
// 更新项目组时间
updateGroupPeriod
(
category
,
groupName
,
newPeriod
)
{
category
.
items
.
forEach
(
item
=>
{
if
(
item
.
name
===
groupName
)
{
item
.
period
=
newPeriod
}
})
},
removeCategoryItem
(
category
,
index
)
{
category
.
items
.
splice
(
index
,
1
)
},
calculateItemTotal
(
item
)
{
item
.
totalPrice
=
(
item
.
unitPrice
||
0
)
*
(
item
.
quantity
||
1
)
},
getCategoryTotal
(
category
)
{
return
category
.
items
.
reduce
((
total
,
item
)
=>
{
return
total
+
(
item
.
totalPrice
||
0
)
},
0
)
},
getCategoryIcon
(
categoryKey
)
{
const
category
=
this
.
allCategories
.
find
(
c
=>
c
.
key
===
categoryKey
)
return
category
?
category
.
icon
:
'el-icon-more'
},
formatPrice
(
price
)
{
if
(
!
price
)
return
'0.00'
return
parseFloat
(
price
).
toLocaleString
(
'zh-CN'
,
{
minimumFractionDigits
:
2
,
maximumFractionDigits
:
2
})
},
async
handleSave
()
{
this
.
$refs
.
form
.
validate
(
async
(
valid
)
=>
{
if
(
!
valid
)
return
this
.
saving
=
true
try
{
const
data
=
{
...
this
.
form
,
itineraryId
:
this
.
itineraryId
,
totalPrice
:
this
.
totalAmount
}
if
(
this
.
isEdit
)
{
await
quoteService
.
updateQuote
({
...
data
,
id
:
this
.
quoteId
})
this
.
$message
.
success
(
'保存成功'
)
}
else
{
await
quoteService
.
createQuote
(
data
)
this
.
$message
.
success
(
'创建成功'
)
}
this
.
$emit
(
'saved'
)
}
catch
(
error
)
{
this
.
$message
.
error
(
'保存失败'
)
}
finally
{
this
.
saving
=
false
}
})
},
handleClose
()
{
this
.
$confirm
(
'确定要关闭吗?未保存的数据将丢失。'
,
'提示'
,
{
confirmButtonText
:
'确定'
,
cancelButtonText
:
'取消'
,
type
:
'warning'
}).
then
(()
=>
{
this
.
drawerVisible
=
false
this
.
$emit
(
'closed'
)
}).
catch
(()
=>
{
// 用户取消
})
},
resetForm
()
{
this
.
form
=
{
quoteName
:
''
,
validDate
:
''
,
categories
:
[]
}
this
.
itineraryInfo
=
{}
this
.
categoryCollapsed
=
{}
}
}
}
</
script
>
<
style
scoped
>
.quote-drawer
{
font-size
:
14px
;
}
.drawer-content
{
height
:
100%
;
/*calc(100vh - 80px); */
/* overflow-y: auto; */
display
:
flex
;
flex-direction
:
column
;
overflow
:
hidden
;
}
.section
{
/* margin-bottom: 12px; */
}
.section-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
/* margin-bottom: 20px; */
padding-bottom
:
10px
;
padding
:
0
20px
10px
20px
;
border-bottom
:
1px
solid
#ebeef5
;
}
.section-header
h3
{
margin
:
0
;
color
:
#303133
;
font-size
:
16px
;
font-weight
:
600
;
}
.total-price
{
font-size
:
18px
;
font-weight
:
bold
;
color
:
#e6a23c
;
}
.quote-categories
{
flex
:
1
;
height
:
100%
;
overflow-y
:
auto
;
padding
:
12px
20px
;
}
.category-section
{
border
:
1px
solid
#ebeef5
;
border-radius
:
8px
;
margin-bottom
:
20px
;
overflow
:
hidden
;
}
.category-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
15px
20px
;
background
:
#f8f9fa
;
border-bottom
:
1px
solid
#ebeef5
;
cursor
:
pointer
;
transition
:
background-color
0.2s
;
}
.category-header
:hover
{
background
:
#f0f2f5
;
}
.category-title
{
display
:
flex
;
align-items
:
center
;
font-weight
:
600
;
color
:
#303133
;
}
.category-title
i
{
margin-right
:
8px
;
font-size
:
16px
;
color
:
#409eff
;
}
.collapse-icon
{
margin-right
:
8px
!important
;
font-size
:
14px
!important
;
color
:
#606266
!important
;
transition
:
transform
0.2s
;
cursor
:
pointer
;
}
.category-items
{
padding
:
20px
;
transition
:
all
0.3s
ease
;
}
/* 聚合项目样式 */
.grouped-item
{
margin-bottom
:
20px
;
border
:
1px
solid
#e4e7ed
;
border-radius
:
8px
;
overflow
:
hidden
;
background
:
#fff
;
}
.item-group-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
12px
16px
;
background
:
#f8f9fa
;
border-bottom
:
1px
solid
#e4e7ed
;
}
.group-info
{
display
:
flex
;
align-items
:
center
;
flex
:
1
;
}
.group-total
{
font-weight
:
bold
;
color
:
#e6a23c
;
margin-left
:
15px
;
}
.group-actions
{
display
:
flex
;
gap
:
8px
;
}
.specifications-list
{
padding
:
0
;
}
.specification-row
{
display
:
flex
;
align-items
:
center
;
padding
:
12px
16px
;
border-bottom
:
1px
solid
#f0f0f0
;
background
:
#fafbfc
;
transition
:
background-color
0.2s
;
}
.specification-row
:hover
{
background
:
#f5f7fa
;
}
.specification-row
:last-child
{
border-bottom
:
none
;
}
.spec-info
{
flex
:
1
;
display
:
flex
;
align-items
:
center
;
}
.spec-price
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.spec-total
{
font-weight
:
bold
;
color
:
#e6a23c
;
min-width
:
80px
;
text-align
:
right
;
}
.spec-actions
{
display
:
flex
;
align-items
:
center
;
margin-left
:
10px
;
}
.category-total
{
margin-left
:
20px
;
color
:
#e6a23c
;
font-weight
:
bold
;
}
.category-actions
{
display
:
flex
;
gap
:
10px
;
}
.quote-item
{
margin-bottom
:
15px
;
padding
:
15px
;
background
:
#fafafa
;
border-radius
:
6px
;
border
:
1px
solid
#f0f0f0
;
}
.item-row
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
}
.item-info
{
flex
:
1
;
display
:
flex
;
align-items
:
center
;
}
.item-price
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.item-total
{
font-weight
:
bold
;
color
:
#e6a23c
;
min-width
:
80px
;
text-align
:
right
;
}
.item-actions
{
display
:
flex
;
align-items
:
center
;
}
.empty-items
{
text-align
:
center
;
padding
:
40px
;
color
:
#909399
;
}
.empty-categories
{
text-align
:
center
;
padding
:
60px
20px
;
}
.drawer-footer
{
padding
:
12px
20px
;
background
:
#fff
;
border-top
:
1px
solid
#ebeef5
;
text-align
:
right
;
}
.danger-btn
{
color
:
#f56c6c
;
}
.danger-btn
:hover
{
color
:
#f56c6c
;
}
/* 响应式调整 */
@media
(
max-width
:
1200px
)
{
.item-row
{
flex-direction
:
column
;
align-items
:
stretch
;
gap
:
15px
;
}
.item-info
{
flex-direction
:
column
;
gap
:
10px
;
}
.item-price
{
justify-content
:
space-between
;
}
}
</
style
>
src/pages/quoted-price/create.vue
0 → 100644
View file @
6d033a4b
<
template
>
<div
class=
"quote-create"
>
<div
class=
"page-header"
>
<h2>
创建报价单
</h2>
<el-button
@
click=
"$router.go(-1)"
>
返回
</el-button>
</div>
<div
class=
"form-container"
>
<el-form
:model=
"form"
ref=
"form"
label-width=
"120px"
class=
"quote-form"
>
<el-form-item
label=
"选择行程"
prop=
"itineraryId"
required
>
<el-select
v-model=
"form.itineraryId"
placeholder=
"请选择行程"
filterable
remote
:remote-method=
"searchItinerary"
:loading=
"itineraryLoading"
style=
"width: 100%"
>
<el-option
v-for=
"item in itineraryOptions"
:key=
"item.id"
:label=
"item.title"
:value=
"item.id"
/>
</el-select>
</el-form-item>
<el-form-item
label=
"报价名称"
prop=
"quoteName"
required
>
<el-input
v-model=
"form.quoteName"
placeholder=
"请输入报价名称"
/>
</el-form-item>
<el-form-item
label=
"报价说明"
prop=
"description"
>
<el-input
type=
"textarea"
v-model=
"form.description"
placeholder=
"请输入报价说明"
:rows=
"4"
/>
</el-form-item>
<el-form-item
label=
"有效期"
prop=
"validDate"
>
<el-date-picker
v-model=
"form.validDate"
type=
"date"
placeholder=
"选择有效期"
value-format=
"yyyy-MM-dd"
/>
</el-form-item>
</el-form>
<div
class=
"form-actions"
>
<el-button
@
click=
"$router.go(-1)"
>
取消
</el-button>
<el-button
type=
"primary"
@
click=
"handleSubmit"
:loading=
"submitting"
>
创建报价单
</el-button>
</div>
</div>
</div>
</
template
>
<
script
>
import
{
quoteService
}
from
'@/services/quote'
import
{
itineraryService
}
from
'@/services/itinerary'
export
default
{
name
:
'QuoteCreate'
,
data
()
{
return
{
form
:
{
itineraryId
:
''
,
quoteName
:
''
,
description
:
''
,
validDate
:
''
},
itineraryOptions
:
[],
itineraryLoading
:
false
,
submitting
:
false
}
},
methods
:
{
async
searchItinerary
(
query
)
{
if
(
!
query
)
return
this
.
itineraryLoading
=
true
try
{
const
response
=
await
itineraryService
.
getItineraryList
({
keyword
:
query
,
pageIndex
:
1
,
pageSize
:
20
})
if
(
response
&&
response
.
data
)
{
this
.
itineraryOptions
=
response
.
data
.
list
||
[]
}
}
catch
(
error
)
{
console
.
error
(
'搜索行程失败:'
,
error
)
}
finally
{
this
.
itineraryLoading
=
false
}
},
async
handleSubmit
()
{
this
.
$refs
.
form
.
validate
(
async
(
valid
)
=>
{
if
(
!
valid
)
return
this
.
submitting
=
true
try
{
await
quoteService
.
createQuote
(
this
.
form
)
this
.
$message
.
success
(
'创建成功'
)
this
.
$router
.
push
(
'/quoted-price/price'
)
}
catch
(
error
)
{
console
.
error
(
'创建失败:'
,
error
)
this
.
$message
.
error
(
'创建失败'
)
}
finally
{
this
.
submitting
=
false
}
})
}
}
}
</
script
>
<
style
scoped
>
.quote-create
{
padding
:
20px
;
background
:
#f5f5f5
;
min-height
:
100vh
;
}
.page-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
20px
;
padding
:
20px
;
background
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
4px
rgba
(
0
,
0
,
0
,
0.1
);
}
.page-header
h2
{
margin
:
0
;
color
:
#333
;
}
.form-container
{
background
:
#fff
;
padding
:
30px
;
border-radius
:
8px
;
box-shadow
:
0
2px
4px
rgba
(
0
,
0
,
0
,
0.1
);
}
.quote-form
{
max-width
:
600px
;
}
.form-actions
{
margin-top
:
30px
;
text-align
:
center
;
}
.form-actions
.el-button
{
margin
:
0
10px
;
min-width
:
100px
;
}
</
style
>
src/pages/quoted-price/edit.vue
0 → 100644
View file @
6d033a4b
<
template
>
<div
class=
"quote-edit"
>
<div
class=
"page-header"
>
<h2>
编辑报价单
</h2>
<el-button
@
click=
"$router.go(-1)"
>
返回
</el-button>
</div>
<div
class=
"form-container"
v-loading=
"loading"
>
<el-form
:model=
"form"
ref=
"form"
label-width=
"120px"
class=
"quote-form"
>
<el-form-item
label=
"行程名称"
>
<span>
{{
form
.
itineraryName
}}
</span>
</el-form-item>
<el-form-item
label=
"报价名称"
prop=
"quoteName"
required
>
<el-input
v-model=
"form.quoteName"
placeholder=
"请输入报价名称"
/>
</el-form-item>
<el-form-item
label=
"报价说明"
prop=
"description"
>
<el-input
type=
"textarea"
v-model=
"form.description"
placeholder=
"请输入报价说明"
:rows=
"4"
/>
</el-form-item>
<el-form-item
label=
"有效期"
prop=
"validDate"
>
<el-date-picker
v-model=
"form.validDate"
type=
"date"
placeholder=
"选择有效期"
value-format=
"yyyy-MM-dd"
/>
</el-form-item>
<el-form-item
label=
"总报价"
prop=
"totalPrice"
>
<el-input-number
v-model=
"form.totalPrice"
:precision=
"2"
:step=
"100"
:min=
"0"
placeholder=
"请输入总报价"
/>
</el-form-item>
<el-form-item
label=
"状态"
prop=
"status"
>
<el-select
v-model=
"form.status"
placeholder=
"请选择状态"
>
<el-option
label=
"草稿"
value=
"draft"
/>
<el-option
label=
"待审核"
value=
"pending"
/>
<el-option
label=
"已通过"
value=
"approved"
/>
<el-option
label=
"已拒绝"
value=
"rejected"
/>
</el-select>
</el-form-item>
</el-form>
<div
class=
"form-actions"
>
<el-button
@
click=
"$router.go(-1)"
>
取消
</el-button>
<el-button
type=
"primary"
@
click=
"handleSubmit"
:loading=
"submitting"
>
保存修改
</el-button>
</div>
</div>
</div>
</
template
>
<
script
>
import
{
quoteService
}
from
'@/services/quote'
export
default
{
name
:
'QuoteEdit'
,
data
()
{
return
{
form
:
{
id
:
''
,
itineraryName
:
''
,
quoteName
:
''
,
description
:
''
,
validDate
:
''
,
totalPrice
:
0
,
status
:
'draft'
},
loading
:
false
,
submitting
:
false
}
},
created
()
{
this
.
loadQuoteDetail
()
},
methods
:
{
async
loadQuoteDetail
()
{
const
id
=
this
.
$route
.
params
.
id
if
(
!
id
)
return
this
.
loading
=
true
try
{
const
response
=
await
quoteService
.
getQuoteDetail
(
id
)
if
(
response
&&
response
.
data
)
{
this
.
form
=
{
...
response
.
data
}
}
}
catch
(
error
)
{
console
.
error
(
'加载报价详情失败:'
,
error
)
this
.
$message
.
error
(
'加载数据失败'
)
}
finally
{
this
.
loading
=
false
}
},
async
handleSubmit
()
{
this
.
$refs
.
form
.
validate
(
async
(
valid
)
=>
{
if
(
!
valid
)
return
this
.
submitting
=
true
try
{
await
quoteService
.
updateQuote
(
this
.
form
)
this
.
$message
.
success
(
'保存成功'
)
this
.
$router
.
push
(
'/quoted-price/price'
)
}
catch
(
error
)
{
console
.
error
(
'保存失败:'
,
error
)
this
.
$message
.
error
(
'保存失败'
)
}
finally
{
this
.
submitting
=
false
}
})
}
}
}
</
script
>
<
style
scoped
>
.quote-edit
{
padding
:
20px
;
background
:
#f5f5f5
;
min-height
:
100vh
;
}
.page-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
20px
;
padding
:
20px
;
background
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
4px
rgba
(
0
,
0
,
0
,
0.1
);
}
.page-header
h2
{
margin
:
0
;
color
:
#333
;
}
.form-container
{
background
:
#fff
;
padding
:
30px
;
border-radius
:
8px
;
box-shadow
:
0
2px
4px
rgba
(
0
,
0
,
0
,
0.1
);
}
.quote-form
{
max-width
:
600px
;
}
.form-actions
{
margin-top
:
30px
;
text-align
:
center
;
}
.form-actions
.el-button
{
margin
:
0
10px
;
min-width
:
100px
;
}
</
style
>
src/pages/quoted-price/price.vue
0 → 100644
View file @
6d033a4b
<
template
>
<div
class=
"price-manager"
>
<BaseListManager
:columns=
"columns"
:dataList=
"dataList"
:loading=
"loading"
:pagination=
"pagination"
:actionColumn=
"actionColumn"
:addActionConfig=
"addActionConfig"
@
add-click=
"handleAddQuote"
@
page-change=
"handlePageChange"
@
size-change=
"handleSizeChange"
@
refresh=
"loadData"
>
<!-- 自定义插槽 -->
<template
#
totalPrice=
"
{ row }">
<span
class=
"price-text"
>
¥
{{
formatPrice
(
row
.
totalPrice
)
}}
</span>
</
template
>
<
template
#
status=
"{ row }"
>
<el-tag
:type=
"getStatusType(row.status)"
size=
"mini"
>
{{
getStatusText
(
row
.
status
)
}}
</el-tag>
</
template
>
<
template
#
actions=
"{ row }"
>
<el-button
type=
"text"
size=
"mini"
@
click=
"handleEdit(row)"
>
<i
class=
"el-icon-edit"
></i>
编辑
</el-button>
<el-button
type=
"text"
size=
"mini"
@
click=
"handleDownload(row)"
>
<i
class=
"el-icon-download"
></i>
下载
</el-button>
<el-button
type=
"text"
size=
"mini"
@
click=
"handleDelete(row)"
class=
"danger-btn"
>
<i
class=
"el-icon-delete"
></i>
删除
</el-button>
</
template
>
</BaseListManager>
<!-- 报价单编辑抽屉 -->
<QuoteDrawer
:visible
.
sync=
"drawerVisible"
:quote-id=
"currentQuoteId"
:itinerary-id=
"currentItineraryId"
@
saved=
"handleQuoteSaved"
@
closed=
"handleDrawerClosed"
/>
</div>
</template>
<
script
>
import
BaseListManager
from
'@/components/common/BaseListManager.vue'
import
QuoteDrawer
from
'./components/QuoteDrawer.vue'
import
{
quoteService
}
from
'@/services/quote'
export
default
{
name
:
'PriceManager'
,
components
:
{
BaseListManager
,
QuoteDrawer
},
data
()
{
return
{
dataList
:
[],
loading
:
false
,
pagination
:
{
currentPage
:
1
,
pageSize
:
20
,
total
:
0
},
// 抽屉相关
drawerVisible
:
false
,
currentQuoteId
:
null
,
currentItineraryId
:
null
,
columns
:
[
{
type
:
'index'
,
label
:
'序号'
,
width
:
60
},
{
prop
:
'itineraryName'
,
label
:
'行程名称'
,
showOverflowTooltip
:
true
},
{
prop
:
'days'
,
label
:
'天数'
,
formatter
:
(
row
)
=>
`
${
row
.
days
}
天`
},
{
prop
:
'hotelCount'
,
label
:
'酒店数量'
,
width
:
100
,
formatter
:
(
row
)
=>
`
${
row
.
hotelCount
}
家`
},
{
prop
:
'ticketCount'
,
label
:
'门票数量'
,
formatter
:
(
row
)
=>
`
${
row
.
ticketCount
}
个`
},
{
prop
:
'mealCount'
,
label
:
'餐饮数量'
,
width
:
100
,
formatter
:
(
row
)
=>
`
${
row
.
mealCount
}
餐`
},
{
prop
:
'transportCount'
,
label
:
'包车次数'
,
formatter
:
(
row
)
=>
`
${
row
.
transportCount
}
次`
},
{
prop
:
'otherItems'
,
label
:
'其它项目'
,
formatter
:
(
row
)
=>
row
.
otherItems
||
'-'
},
{
prop
:
'totalPrice'
,
label
:
'总报价'
,
slotName
:
'totalPrice'
},
// {
// prop: 'status',
// label: '状态',
// width: 100,
// slotName: 'status'
// },
{
prop
:
'createTime'
,
label
:
'创建时间'
,
formatter
:
(
row
)
=>
this
.
formatDate
(
row
.
createTime
)
}
],
actionColumn
:
{
label
:
'操作'
,
fixed
:
'right'
},
addActionConfig
:
{
buttonText
:
'创建报价单'
}
}
},
created
()
{
this
.
loadData
()
},
methods
:
{
async
loadData
()
{
this
.
loading
=
true
try
{
const
params
=
{
pageIndex
:
this
.
pagination
.
currentPage
,
pageSize
:
this
.
pagination
.
pageSize
}
const
response
=
await
quoteService
.
getQuoteList
(
params
)
if
(
response
&&
response
.
data
)
{
// 处理数据,计算各项统计
const
processedData
=
(
response
.
data
.
list
||
[]).
map
(
item
=>
{
return
{
...
item
,
// 基于行程数据计算统计信息
days
:
this
.
calculateDays
(
item
.
itinerary
),
hotelCount
:
this
.
calculateHotelCount
(
item
.
itinerary
),
ticketCount
:
this
.
calculateTicketCount
(
item
.
itinerary
),
mealCount
:
this
.
calculateMealCount
(
item
.
itinerary
),
transportCount
:
this
.
calculateTransportCount
(
item
.
itinerary
),
otherItems
:
this
.
calculateOtherItems
(
item
.
itinerary
)
}
})
this
.
dataList
=
processedData
this
.
pagination
.
total
=
response
.
data
.
total
||
0
}
}
catch
(
error
)
{
console
.
error
(
'加载报价列表失败:'
,
error
)
this
.
$message
.
error
(
'加载数据失败'
)
// 如果API还未实现,使用模拟数据进行演示
this
.
loadMockData
()
}
finally
{
this
.
loading
=
false
}
},
// 模拟数据用于演示
loadMockData
()
{
this
.
dataList
=
[
{
id
:
'1'
,
itineraryName
:
'九寨沟黄龙3日游'
,
days
:
3
,
hotelCount
:
2
,
ticketCount
:
5
,
mealCount
:
6
,
transportCount
:
2
,
otherItems
:
'导游服务'
,
totalPrice
:
2580.00
,
status
:
'approved'
,
createTime
:
'2024-01-15 10:30:00'
},
{
id
:
'2'
,
itineraryName
:
'成都熊猫基地一日游'
,
days
:
1
,
hotelCount
:
0
,
ticketCount
:
2
,
mealCount
:
2
,
transportCount
:
1
,
otherItems
:
'接送服务'
,
totalPrice
:
380.00
,
status
:
'pending'
,
createTime
:
'2024-01-16 14:20:00'
},
{
id
:
'3'
,
itineraryName
:
'稻城亚丁7日深度游'
,
days
:
7
,
hotelCount
:
6
,
ticketCount
:
8
,
mealCount
:
14
,
transportCount
:
3
,
otherItems
:
'高原氧气'
,
totalPrice
:
4680.00
,
status
:
'draft'
,
createTime
:
'2024-01-17 09:15:00'
}
]
this
.
pagination
.
total
=
3
},
// 计算天数
calculateDays
(
itinerary
)
{
if
(
!
itinerary
||
!
itinerary
.
days
)
return
0
return
itinerary
.
days
.
length
},
// 计算酒店数量
calculateHotelCount
(
itinerary
)
{
if
(
!
itinerary
||
!
itinerary
.
days
)
return
0
let
count
=
0
itinerary
.
days
.
forEach
(
day
=>
{
if
(
day
.
hotels
&&
day
.
hotels
.
length
>
0
)
{
count
+=
day
.
hotels
.
length
}
})
return
count
},
// 计算门票数量
calculateTicketCount
(
itinerary
)
{
if
(
!
itinerary
||
!
itinerary
.
days
)
return
0
let
count
=
0
itinerary
.
days
.
forEach
(
day
=>
{
if
(
day
.
attractions
&&
day
.
attractions
.
length
>
0
)
{
count
+=
day
.
attractions
.
length
}
})
return
count
},
// 计算餐饮数量
calculateMealCount
(
itinerary
)
{
if
(
!
itinerary
||
!
itinerary
.
days
)
return
0
let
count
=
0
itinerary
.
days
.
forEach
(
day
=>
{
if
(
day
.
caterings
&&
day
.
caterings
.
length
>
0
)
{
count
+=
day
.
caterings
.
length
}
})
return
count
},
// 计算包车次数
calculateTransportCount
(
itinerary
)
{
if
(
!
itinerary
||
!
itinerary
.
days
)
return
0
let
count
=
0
itinerary
.
days
.
forEach
(
day
=>
{
if
(
day
.
traffics
&&
day
.
traffics
.
length
>
0
)
{
// 只统计非航班的交通方式
count
+=
day
.
traffics
.
filter
(
t
=>
t
.
type
!==
'航班'
).
length
}
})
return
count
},
// 计算其他项目
calculateOtherItems
(
itinerary
)
{
if
(
!
itinerary
)
return
'-'
const
items
=
[]
// 可以根据行程中的其他信息来判断
if
(
itinerary
.
include
)
items
.
push
(
'包含项目'
)
if
(
itinerary
.
notInclude
)
items
.
push
(
'不含项目'
)
if
(
itinerary
.
warning
)
items
.
push
(
'注意事项'
)
return
items
.
length
>
0
?
items
.
join
(
', '
)
:
'-'
},
handleAddQuote
()
{
// 打开新增抽屉,可以传入行程ID
this
.
currentQuoteId
=
null
this
.
currentItineraryId
=
this
.
$route
.
query
.
itineraryId
||
'2f09262b-c4aa-4e53-b5ff-178254ecbcf4'
this
.
drawerVisible
=
true
},
handleEdit
(
row
)
{
// 打开编辑抽屉
this
.
currentQuoteId
=
row
.
id
this
.
currentItineraryId
=
row
.
itineraryId
this
.
drawerVisible
=
true
},
handleQuoteSaved
()
{
// 报价单保存后刷新列表
this
.
loadData
()
this
.
drawerVisible
=
false
},
handleDrawerClosed
()
{
// 抽屉关闭后清理状态
this
.
currentQuoteId
=
null
this
.
currentItineraryId
=
null
},
async
handleDownload
(
row
)
{
try
{
this
.
$message
.
info
(
'正在生成报价单...'
)
const
response
=
await
quoteService
.
downloadQuote
(
row
.
id
)
// 创建下载链接
const
blob
=
new
Blob
([
response
],
{
type
:
'application/pdf'
})
const
url
=
window
.
URL
.
createObjectURL
(
blob
)
const
link
=
document
.
createElement
(
'a'
)
link
.
href
=
url
link
.
download
=
`
${
row
.
itineraryName
}
_报价单.pdf`
document
.
body
.
appendChild
(
link
)
link
.
click
()
document
.
body
.
removeChild
(
link
)
window
.
URL
.
revokeObjectURL
(
url
)
this
.
$message
.
success
(
'下载成功'
)
}
catch
(
error
)
{
console
.
error
(
'下载失败:'
,
error
)
this
.
$message
.
error
(
'下载失败'
)
}
},
handleDelete
(
row
)
{
this
.
$confirm
(
`确定要删除报价单"
${
row
.
itineraryName
}
"吗?`
,
'提示'
,
{
confirmButtonText
:
'确定'
,
cancelButtonText
:
'取消'
,
type
:
'warning'
}).
then
(
async
()
=>
{
try
{
await
quoteService
.
deleteQuote
(
row
.
id
)
this
.
$message
.
success
(
'删除成功'
)
this
.
loadData
()
}
catch
(
error
)
{
console
.
error
(
'删除失败:'
,
error
)
this
.
$message
.
error
(
'删除失败'
)
}
}).
catch
(()
=>
{
// 用户取消删除
})
},
handlePageChange
(
page
)
{
this
.
pagination
.
currentPage
=
page
this
.
loadData
()
},
handleSizeChange
(
size
)
{
this
.
pagination
.
pageSize
=
size
this
.
pagination
.
currentPage
=
1
this
.
loadData
()
},
formatPrice
(
price
)
{
if
(
!
price
)
return
'0.00'
return
parseFloat
(
price
).
toLocaleString
(
'zh-CN'
,
{
minimumFractionDigits
:
2
,
maximumFractionDigits
:
2
})
},
formatDate
(
dateStr
)
{
if
(
!
dateStr
)
return
'-'
const
date
=
new
Date
(
dateStr
)
return
date
.
toLocaleString
(
'zh-CN'
,
{
year
:
'numeric'
,
month
:
'2-digit'
,
day
:
'2-digit'
,
hour
:
'2-digit'
,
minute
:
'2-digit'
})
},
getStatusType
(
status
)
{
const
statusMap
=
{
'draft'
:
'info'
,
'pending'
:
'warning'
,
'approved'
:
'success'
,
'rejected'
:
'danger'
}
return
statusMap
[
status
]
||
'info'
},
getStatusText
(
status
)
{
const
statusMap
=
{
'draft'
:
'草稿'
,
'pending'
:
'待审核'
,
'approved'
:
'已通过'
,
'rejected'
:
'已拒绝'
}
return
statusMap
[
status
]
||
'未知'
}
}
}
</
script
>
<
style
scoped
>
.price-manager
{
padding
:
20px
;
background
:
#f5f5f5
;
min-height
:
100vh
;
}
.price-text
{
color
:
#e6a23c
;
font-size
:
14px
;
}
.danger-btn
{
color
:
#f56c6c
;
}
.danger-btn
:hover
{
color
:
#f56c6c
;
}
</
style
>
\ No newline at end of file
src/plug/index.js
View file @
6d033a4b
...
...
@@ -123,7 +123,7 @@ export default {
let
isOnline
=
0
;
//0-本地测试,1-线上
let
ocrUrl
=
"http://192.168.5.46:8888"
;
// domainUrl = "http://192.168.5.214";
domainUrl
=
"http://192.168.5.
39:8083
"
domainUrl
=
"http://192.168.5.
214
"
// domainUrl = "http://192.168.5.204:8030"
// domainUrl = "http://reborn.oytour.com";
let
crmLocalFileStreamDownLoadUrl
=
""
;
...
...
@@ -1989,12 +1989,12 @@ export default {
}
Vue
.
prototype
.
deepFirstLetterToLower
=
function
(
obj
)
{
if
(
typeof
obj
!==
'object'
||
obj
===
null
)
return
obj
;
// 非对象直接返回
// 处理数组:遍历每个元素递归转换
if
(
Array
.
isArray
(
obj
))
{
return
obj
.
map
(
item
=>
deepFirstLetterToLower
(
item
));
}
// 处理普通对象:遍历键值对递归转换
const
newObj
=
{};
for
(
const
key
in
obj
)
{
...
...
@@ -2005,7 +2005,7 @@ export default {
}
return
newObj
;
}
// 基础首字母转小写函数(同上)
Vue
.
prototype
.
firstLetterToLower
=
function
(
str
)
{
if
(
typeof
str
!==
'string'
||
str
.
length
===
0
)
return
''
;
...
...
src/router/config.js
View file @
6d033a4b
...
...
@@ -6832,5 +6832,29 @@ export default {
title
:
'编辑行程'
}
},
{
path
:
'/quoted-price/price'
,
name
:
'quotedPrice'
,
component
:
resolve
=>
require
([
'@/pages/quoted-price/price'
],
resolve
),
meta
:
{
title
:
'报价管理'
}
},
{
path
:
'/quoted-price/create'
,
name
:
'quotedPriceCreate'
,
component
:
resolve
=>
require
([
'@/pages/quoted-price/create'
],
resolve
),
meta
:
{
title
:
'创建报价单'
}
},
{
path
:
'/quoted-price/edit/:id'
,
name
:
'quotedPriceEdit'
,
component
:
resolve
=>
require
([
'@/pages/quoted-price/edit'
],
resolve
),
meta
:
{
title
:
'编辑报价单'
}
},
]
}
src/services/quote.js
0 → 100644
View file @
6d033a4b
import
rwRequest
from
'@/plug/rwRequest'
;
export
const
quoteService
=
{
/**
* 获取报价列表
* @param {Object} params - 查询参数
* @returns {Promise} - API 响应
*/
getQuoteList
:
(
params
)
=>
{
return
rwRequest
.
get
(
'/quote/list'
,
{
params
});
},
/**
* 获取报价详情
* @param {string} id - 报价ID
* @returns {Promise} - API 响应
*/
getQuoteDetail
:
(
id
)
=>
{
return
rwRequest
.
get
(
`/quote/
${
id
}
`
);
},
/**
* 创建报价
* @param {Object} data - 报价数据
* @returns {Promise} - API 响应
*/
createQuote
:
(
data
)
=>
{
return
rwRequest
.
post
(
'/quote'
,
data
);
},
/**
* 更新报价
* @param {Object} data - 报价数据
* @returns {Promise} - API 响应
*/
updateQuote
:
(
data
)
=>
{
return
rwRequest
.
put
(
'/quote'
,
data
);
},
/**
* 删除报价
* @param {string} id - 报价ID
* @returns {Promise} - API 响应
*/
deleteQuote
:
(
id
)
=>
{
return
rwRequest
.
delete
(
`/quote/
${
id
}
`
);
},
/**
* 下载报价单
* @param {string} id - 报价ID
* @returns {Promise} - API 响应
*/
downloadQuote
:
(
id
)
=>
{
return
rwRequest
.
get
(
`/quote/
${
id
}
/download`
,
{
responseType
:
'blob'
});
},
/**
* 根据行程ID生成报价
* @param {string} itineraryId - 行程ID
* @returns {Promise} - API 响应
*/
generateQuoteFromItinerary
:
(
itineraryId
)
=>
{
return
rwRequest
.
post
(
`/quote/generate/
${
itineraryId
}
`
);
},
/**
* 审核报价
* @param {string} id - 报价ID
* @param {string} status - 审核状态 (approved/rejected)
* @param {string} remark - 审核备注
* @returns {Promise} - API 响应
*/
reviewQuote
:
(
id
,
status
,
remark
)
=>
{
return
rwRequest
.
post
(
`/quote/
${
id
}
/review`
,
{
status
,
remark
});
},
/**
* 根据行程ID获取行程详情(用于报价单生成)
* @param {string} itineraryId - 行程ID
* @returns {Promise} - API 响应
*/
getItineraryForQuote
:
(
itineraryId
)
=>
{
return
rwRequest
.
get
(
`/itinerary/
${
itineraryId
}
/for-quote`
);
},
/**
* 复制报价单
* @param {string} id - 报价ID
* @returns {Promise} - API 响应
*/
copyQuote
:
(
id
)
=>
{
return
rwRequest
.
post
(
`/quote/
${
id
}
/copy`
);
}
};
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