Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation
This project
Loading...
Sign in
李宁
/
Activity
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit e21e4653
authored
Nov 04, 2025
by
李宁
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
1
1 parent
7df3f72d
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
524 additions
and
441 deletions
zhiJianBusi/zjbPc/.claude/settings.local.json
zhiJianBusi/zjbPc/src/components/system/EnterprisePersonnelManagement.vue
zhiJianBusi/zjbPc/src/components/system/PersonnelManagement.vue
zhiJianBusi/zjbPc/src/views/GridQuery.vue
zhiJianBusi/zjbPc/src/views/LoginPage.vue
zhiJianBusi/zjbPc/src/views/OpportunityManagement.vue
zhiJianBusi/zjbPc/.claude/settings.local.json
View file @
e21e465
{
"permissions"
:
{
"allow"
:
[
"Bash(tree src/ -I node_modules)"
"Bash(tree src/ -I node_modules)"
,
"Bash(npm run serve)"
],
"deny"
:
[],
"ask"
:
[]
...
...
zhiJianBusi/zjbPc/src/components/system/EnterprisePersonnelManagement.vue
View file @
e21e465
...
...
@@ -6,7 +6,14 @@
<div
class=
"search-wrapper"
>
<el-input
v-model=
"searchQuery"
placeholder=
"请输入工号/手机号查询"
placeholder=
"请输入工号查询"
class=
"search-input"
/>
</div>
<div
class=
"search-wrapper"
>
<el-input
v-model=
"searchPhone"
placeholder=
"请输入手机号查询"
class=
"search-input"
/>
</div>
...
...
@@ -218,6 +225,7 @@ export default {
return
{
personnel
:
mockEnterprisePersonnel
,
searchQuery
:
''
,
searchPhone
:
''
,
selectedRegion
:
[],
selectedRegionDistrict
:
''
,
isAddDialogOpen
:
false
,
...
...
zhiJianBusi/zjbPc/src/components/system/PersonnelManagement.vue
View file @
e21e465
...
...
@@ -10,6 +10,13 @@
class=
"search-input"
/>
</div>
<div
class=
"search-wrapper"
>
<el-input
v-model=
"searchPhone"
placeholder=
"请输入手机号查询..."
class=
"search-input"
/>
</div>
<el-select
v-model=
"filterType"
placeholder=
"人员类型"
class=
"filter-select"
>
<el-option
label=
"全部类型"
value=
"all"
></el-option>
<el-option
label=
"装维师傅"
value=
"installer"
></el-option>
...
...
@@ -309,6 +316,7 @@ export default {
personnelTypeMap
,
personnel
:
mockPersonnel
,
searchTerm
:
''
,
searchPhone
:
''
,
filterType
:
'all'
,
filterRegion
:
''
,
isAddDialogOpen
:
false
,
...
...
zhiJianBusi/zjbPc/src/views/GridQuery.vue
View file @
e21e465
...
...
@@ -12,10 +12,6 @@
<i
class=
"el-icon-search"
></i>
查询
</el-button>
<el-button
size=
"small"
@
click=
"handleExport"
type=
"default"
>
<i
class=
"el-icon-download"
></i>
导出数据
</el-button>
<el-button
size=
"small"
@
click=
"resetQuery"
type=
"default"
>
<i
class=
"el-icon-refresh-left"
></i>
重置
...
...
@@ -37,121 +33,30 @@
</el-col>
<el-col
:span=
"3"
>
<el-select
v-model=
"selectedGrid"
placeholder=
"选择网格"
clearable
>
<el-option
label=
"全部网格"
value=
""
></el-option>
<el-option
label=
"A网格"
value=
"A网格"
></el-option>
<el-option
label=
"B网格"
value=
"B网格"
></el-option>
<el-option
label=
"C网格"
value=
"C网格"
></el-option>
<el-option
label=
"D网格"
value=
"D网格"
></el-option>
<el-option
label=
"E网格"
value=
"E网格"
></el-option>
</el-select>
</el-col>
<el-col
:span=
"3"
>
<el-select
v-model=
"selectedInstaller"
placeholder=
"选择装维师傅"
clearable
>
<el-option
label=
"全部师傅"
value=
""
></el-option>
<el-option
v-for=
"installer in installers"
:key=
"installer.id"
:label=
"`$
{installer.name} (${installer.workId})`"
:value="installer.id"
>
</el-option>
</el-select>
<div
class=
"search-wrapper"
>
<el-input
v-model=
"selectedGridName"
placeholder=
"请输入网格名称"
style=
"padding-left: 0;"
@
keyup
.
enter
.
native=
"handleFilter"
/>
</div>
</el-col>
<el-col
:span=
"3"
>
<el-select
v-model=
"selectedSales"
placeholder=
"选择营销人员"
clearable
>
<el-option
label=
"全部人员"
value=
""
></el-option>
<el-option
v-for=
"sales in salesPersons"
:key=
"sales.id"
:label=
"`$
{sales.name} (${sales.workId})`"
:value="sales.id"
>
</el-option>
</el-select>
<div
class=
"search-wrapper"
>
<el-input
v-model=
"selectedGrid"
placeholder=
"请输入网格ID"
style=
"padding-left: 0;"
@
keyup
.
enter
.
native=
"handleFilter"
/>
</div>
</el-col>
</el-row>
</div>
</el-card>
<!-- 网格数据统计 -->
<div
class=
"stats-cards"
>
<el-row
:gutter=
"24"
>
<el-col
:span=
"6"
>
<el-card
class=
"dashboard-card"
>
<div
class=
"card-header"
>
<span
class=
"card-title"
>
网格总数
</span>
</div>
<div
class=
"card-content"
>
<div
class=
"card-value"
>
{{
gridStats
.
totalGrids
}}
</div>
</div>
<img
src=
"../assets/icons/ad61df2f28f6b51d5f386829473ab1b592fd14e0.png"
alt=
""
class=
"card-icon"
>
</el-card>
</el-col>
<el-col
:span=
"6"
>
<el-card
class=
"dashboard-card"
>
<div
class=
"card-header"
>
<span
class=
"card-title"
>
装维师傅
</span>
</div>
<div
class=
"card-content"
>
<div
class=
"card-value"
>
{{
gridStats
.
totalInstallers
}}
</div>
</div>
<img
src=
"../assets/icons/a8703fd7713f624a505aed79bcf30eb245a86d9c.png"
alt=
""
class=
"card-icon"
>
</el-card>
</el-col>
<el-col
:span=
"6"
>
<el-card
class=
"dashboard-card"
>
<div
class=
"card-header"
>
<span
class=
"card-title"
>
营销人员
</span>
</div>
<div
class=
"card-content"
>
<div
class=
"card-value"
>
{{
gridStats
.
totalSales
}}
</div>
</div>
<img
src=
"../assets/icons/eb05b4822d67dff64c5712d0777f069d241ecc13.png"
alt=
""
class=
"card-icon"
>
</el-card>
</el-col>
<el-col
:span=
"6"
>
<el-card
class=
"dashboard-card"
>
<div
class=
"card-header"
>
<span
class=
"card-title"
>
商机总数
</span>
</div>
<div
class=
"card-content"
>
<div
class=
"card-value"
>
{{
gridStats
.
totalOpportunities
}}
</div>
</div>
<img
src=
"../assets/icons/1b66793397a66bf54212d266505eb98e3377a354.png"
alt=
""
class=
"card-icon"
>
</el-card>
</el-col>
</el-row>
</div>
<!-- 网格列表 -->
<el-card
class=
"grid-list-card"
>
<div
class=
"list-header"
>
...
...
@@ -160,45 +65,12 @@
<div
class=
"list-content"
>
<el-table
:data=
"
filteredGrids
"
:data=
"
tableData
"
border
style=
"width: 100%"
@
row-click=
"handleRowClick"
>
<el-table-column
prop=
"name"
label=
"网格名称"
width=
"120"
></el-table-column>
<el-table-column
prop=
"region"
label=
"所属区域"
width=
"250"
></el-table-column>
<el-table-column
prop=
"installerCount"
label=
"装维师傅数"
width=
"120"
></el-table-column>
<el-table-column
prop=
"salesCount"
label=
"营销人员数"
width=
"120"
></el-table-column>
<el-table-column
prop=
"opportunityCount"
label=
"商机数"
width=
"100"
></el-table-column>
<el-table-column
prop=
"status"
label=
"状态"
width=
"100"
>
<template
slot-scope=
"scope"
>
<el-tag
:type=
"scope.row.status === 'active' ? 'success' : 'info'"
size=
"small"
>
{{
scope
.
row
.
status
===
'active'
?
'活跃'
:
'非活跃'
}}
</el-tag>
</
template
>
</el-table-column>
<el-table-column
prop=
"lastActivity"
label=
"最后活跃"
width=
"200"
></el-table-column>
<el-table-column
label=
"操作"
>
<
template
slot-scope=
"scope"
>
<el-button
type=
"text"
size=
"small"
@
click
.
stop=
"viewGridDetail(scope.row)"
>
查看详情
</el-button>
<el-button
type=
"text"
size=
"small"
@
click
.
stop=
"editGrid(scope.row)"
>
编辑
</el-button>
</
template
>
</el-table-column>
<el-table-column
prop=
"name"
label=
"网格名称"
width=
"220"
></el-table-column>
<el-table-column
prop=
"id"
label=
"网格ID"
width=
"220"
></el-table-column>
<el-table-column
prop=
"region"
label=
"所属区域"
min-width=
"250"
></el-table-column>
</el-table>
<!-- 分页 -->
...
...
@@ -206,309 +78,49 @@
<el-pagination
@
size-change=
"handleSizeChange"
@
current-change=
"handleCurrentChange"
:current-page=
"currentPage"
:current-page=
"
pageStore.
currentPage"
:page-sizes=
"[20, 50, 100]"
:page-size=
"pageSize"
:page-size=
"pageS
tore.pageS
ize"
layout=
"total, sizes, prev, pager, next, jumper"
:total=
"
filteredGrids.length
"
:total=
"
pageStore.total
"
>
</el-pagination>
</div>
</div>
</el-card>
<!-- 网格详情对话框 -->
<el-dialog
title=
"网格详情"
:visible
.
sync=
"detailDialogVisible"
width=
"800px"
>
<div
v-if=
"selectedGridDetail"
class=
"grid-detail"
>
<div
class=
"detail-header"
>
<h3>
{{ selectedGridDetail.name }} - {{ selectedGridDetail.region }}
</h3>
</div>
<el-row
:gutter=
"20"
class=
"detail-stats"
>
<el-col
:span=
"6"
>
<div
class=
"stat-item"
>
<div
class=
"stat-value"
>
{{ selectedGridDetail.installerCount }}
</div>
<div
class=
"stat-label"
>
装维师傅
</div>
</div>
</el-col>
<el-col
:span=
"6"
>
<div
class=
"stat-item"
>
<div
class=
"stat-value"
>
{{ selectedGridDetail.salesCount }}
</div>
<div
class=
"stat-label"
>
营销人员
</div>
</div>
</el-col>
<el-col
:span=
"6"
>
<div
class=
"stat-item"
>
<div
class=
"stat-value"
>
{{ selectedGridDetail.opportunityCount }}
</div>
<div
class=
"stat-label"
>
商机数
</div>
</div>
</el-col>
<el-col
:span=
"6"
>
<div
class=
"stat-item"
>
<div
class=
"stat-value"
>
{{ selectedGridDetail.completionRate }}%
</div>
<div
class=
"stat-label"
>
成单率
</div>
</div>
</el-col>
</el-row>
<div
class=
"detail-content"
>
<el-tabs
v-model=
"activeTab"
>
<el-tab-pane
label=
"装维师傅"
name=
"installers"
>
<el-table
:data=
"selectedGridDetail.installers"
style=
"width: 100%"
>
<el-table-column
prop=
"name"
label=
"姓名"
width=
"120"
></el-table-column>
<el-table-column
prop=
"workId"
label=
"工号"
width=
"100"
></el-table-column>
<el-table-column
prop=
"phone"
label=
"联系方式"
width=
"120"
></el-table-column>
<el-table-column
prop=
"opportunityCount"
label=
"商机数"
width=
"100"
></el-table-column>
<el-table-column
prop=
"completionRate"
label=
"成单率"
width=
"100"
>
<
template
slot-scope=
"scope"
>
{{
scope
.
row
.
completionRate
}}
%
</
template
>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane
label=
"营销人员"
name=
"sales"
>
<el-table
:data=
"selectedGridDetail.sales"
style=
"width: 100%"
>
<el-table-column
prop=
"name"
label=
"姓名"
width=
"120"
></el-table-column>
<el-table-column
prop=
"workId"
label=
"工号"
width=
"100"
></el-table-column>
<el-table-column
prop=
"phone"
label=
"联系方式"
width=
"120"
></el-table-column>
<el-table-column
prop=
"opportunityCount"
label=
"商机数"
width=
"100"
></el-table-column>
<el-table-column
prop=
"completionRate"
label=
"成单率"
width=
"100"
>
<
template
slot-scope=
"scope"
>
{{
scope
.
row
.
completionRate
}}
%
</
template
>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane
label=
"商机明细"
name=
"opportunities"
>
<el-table
:data=
"selectedGridDetail.opportunities"
style=
"width: 100%"
>
<el-table-column
prop=
"id"
label=
"商机ID"
width=
"120"
></el-table-column>
<el-table-column
prop=
"customerAddress"
label=
"客户地址"
width=
"200"
></el-table-column>
<el-table-column
prop=
"installerName"
label=
"装维师傅"
width=
"120"
></el-table-column>
<el-table-column
prop=
"salesName"
label=
"营销人员"
width=
"120"
></el-table-column>
<el-table-column
prop=
"createTime"
label=
"创建时间"
width=
"150"
></el-table-column>
<el-table-column
prop=
"status"
label=
"状态"
width=
"100"
>
<
template
slot-scope=
"scope"
>
<el-tag
:type=
"getStatusType(scope.row.status)"
size=
"small"
>
{{
getStatusLabel
(
scope
.
row
.
status
)
}}
</el-tag>
</
template
>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</div>
</el-dialog>
</div>
</
template
>
<
script
>
// 模拟网格数据
const
mockGrids
=
[
{
id
:
'GRID001'
,
name
:
'A网格'
,
region
:
'江苏省-南京市-玄武区'
,
installerCount
:
8
,
salesCount
:
6
,
opportunityCount
:
45
,
status
:
'active'
,
lastActivity
:
'2025-09-28 15:30:00'
},
{
id
:
'GRID002'
,
name
:
'B网格'
,
region
:
'江苏省-南京市-玄武区'
,
installerCount
:
7
,
salesCount
:
5
,
opportunityCount
:
38
,
status
:
'active'
,
lastActivity
:
'2025-09-28 14:20:00'
},
{
id
:
'GRID003'
,
name
:
'C网格'
,
region
:
'江苏省-南京市-秦淮区'
,
installerCount
:
9
,
salesCount
:
7
,
opportunityCount
:
52
,
status
:
'active'
,
lastActivity
:
'2025-09-28 12:15:00'
},
{
id
:
'GRID004'
,
name
:
'D网格'
,
region
:
'江苏省-南京市-建邺区'
,
installerCount
:
6
,
salesCount
:
4
,
opportunityCount
:
31
,
status
:
'active'
,
lastActivity
:
'2025-09-27 18:45:00'
},
{
id
:
'GRID005'
,
name
:
'E网格'
,
region
:
'江苏省-南京市-鼓楼区'
,
installerCount
:
10
,
salesCount
:
8
,
opportunityCount
:
63
,
status
:
'active'
,
lastActivity
:
'2025-09-28 16:20:00'
}
]
// 模拟人员数据
const
mockInstallers
=
[
{
id
:
'INS001'
,
name
:
'王师傅'
,
workId
:
'W001'
,
phone
:
'13987654321'
,
gridId
:
'GRID001'
,
opportunityCount
:
12
,
completionRate
:
75
},
{
id
:
'INS002'
,
name
:
'李师傅'
,
workId
:
'W002'
,
phone
:
'13876543210'
,
gridId
:
'GRID001'
,
opportunityCount
:
9
,
completionRate
:
66
},
{
id
:
'INS003'
,
name
:
'张师傅'
,
workId
:
'W003'
,
phone
:
'13765432109'
,
gridId
:
'GRID002'
,
opportunityCount
:
11
,
completionRate
:
72
},
{
id
:
'INS004'
,
name
:
'刘师傅'
,
workId
:
'W004'
,
phone
:
'13654321098'
,
gridId
:
'GRID002'
,
opportunityCount
:
8
,
completionRate
:
62
},
{
id
:
'INS005'
,
name
:
'陈师傅'
,
workId
:
'W005'
,
phone
:
'13543210987'
,
gridId
:
'GRID003'
,
opportunityCount
:
15
,
completionRate
:
80
}
]
const
mockSalesPersons
=
[
{
id
:
'SALE001'
,
name
:
'张营销'
,
workId
:
'S001'
,
phone
:
'13912345678'
,
gridId
:
'GRID001'
,
opportunityCount
:
10
,
completionRate
:
80
},
{
id
:
'SALE002'
,
name
:
'陈营销'
,
workId
:
'S002'
,
phone
:
'13923456789'
,
gridId
:
'GRID001'
,
opportunityCount
:
8
,
completionRate
:
75
},
{
id
:
'SALE003'
,
name
:
'杨营销'
,
workId
:
'S003'
,
phone
:
'13934567890'
,
gridId
:
'GRID002'
,
opportunityCount
:
12
,
completionRate
:
83
},
{
id
:
'SALE004'
,
name
:
'黄营销'
,
workId
:
'S004'
,
phone
:
'13945678901'
,
gridId
:
'GRID002'
,
opportunityCount
:
7
,
completionRate
:
71
},
{
id
:
'SALE005'
,
name
:
'徐营销'
,
workId
:
'S005'
,
phone
:
'13956789012'
,
gridId
:
'GRID003'
,
opportunityCount
:
14
,
completionRate
:
85
}
]
// 模拟商机数据
const
mockOpportunities
=
[
{
id
:
'OP202500001'
,
customerAddress
:
'南京市玄武区中山路123号'
,
installerName
:
'王师傅'
,
salesName
:
'张营销'
,
createTime
:
'2025-09-27 10:30:00'
,
status
:
'assigned'
,
gridId
:
'GRID001'
},
{
id
:
'OP202500002'
,
customerAddress
:
'南京市玄武区珠江路456号'
,
installerName
:
'李师傅'
,
salesName
:
'陈营销'
,
createTime
:
'2025-09-27 14:20:00'
,
status
:
'following'
,
gridId
:
'GRID001'
},
{
id
:
'OP202500003'
,
customerAddress
:
'南京市秦淮区北京东路789号'
,
installerName
:
'张师傅'
,
salesName
:
'杨营销'
,
createTime
:
'2025-09-26 16:45:00'
,
status
:
'completed'
,
gridId
:
'GRID002'
},
{
id
:
'OP202500004'
,
customerAddress
:
'南京市建邺区汉中路321号'
,
installerName
:
'刘师傅'
,
salesName
:
'黄营销'
,
createTime
:
'2025-09-28 09:15:00'
,
status
:
'assigned'
,
gridId
:
'GRID002'
}
]
export
default
{
name
:
'GridQuery'
,
data
()
{
return
{
selectedRegion
:
[],
selectedGrid
:
''
,
selectedInstaller
:
''
,
selectedSales
:
''
,
regionOptions
:
''
,
grids
:
mockGrids
,
installers
:
mockInstallers
,
salesPersons
:
mockSalesPersons
,
opportunities
:
mockOpportunities
,
gridStats
:
{
totalGrids
:
5
,
totalInstallers
:
28
,
totalSales
:
22
,
totalOpportunities
:
239
},
currentPage
:
1
,
pageSize
:
20
,
detailDialogVisible
:
false
,
selectedGridDetail
:
null
,
activeTab
:
'installers'
selectedGridName
:
''
,
tableData
:
[],
pageStore
:{
currentPage
:
1
,
pageSize
:
20
,
total
:
0
}
}
},
created
(){
this
.
regionOptions
=
this
.
addressStoreData
},
computed
:
{
filteredGrids
()
{
let
result
=
this
.
grids
// 区域筛选
if
(
this
.
selectedRegion
&&
this
.
selectedRegion
.
length
>
0
)
{
const
regionPath
=
this
.
selectedRegion
.
join
(
'-'
)
result
=
result
.
filter
(
grid
=>
grid
.
region
.
startsWith
(
regionPath
))
}
// 网格筛选
if
(
this
.
selectedGrid
)
{
result
=
result
.
filter
(
grid
=>
grid
.
name
===
this
.
selectedGrid
)
}
// 装维师傅筛选
if
(
this
.
selectedInstaller
)
{
// 获取该师傅所属的网格
const
installerGrid
=
this
.
installers
.
find
(
inst
=>
inst
.
id
===
this
.
selectedInstaller
)?.
gridId
if
(
installerGrid
)
{
result
=
result
.
filter
(
grid
=>
grid
.
id
===
installerGrid
)
}
}
// 营销人员筛选
if
(
this
.
selectedSales
)
{
// 获取该人员所属的网格
const
salesGrid
=
this
.
salesPersons
.
find
(
sales
=>
sales
.
id
===
this
.
selectedSales
)?.
gridId
if
(
salesGrid
)
{
result
=
result
.
filter
(
grid
=>
grid
.
id
===
salesGrid
)
}
}
return
result
}
},
methods
:
{
handleQuery
()
{
this
.
currentPage
=
1
this
.
$message
.
success
(
'查询成功'
)
},
handleExport
()
{
this
.
$message
.
success
(
'数据导出成功'
)
},
resetQuery
()
{
this
.
selectedRegion
=
[]
this
.
selectedGrid
=
''
this
.
selectedInstaller
=
''
this
.
selectedSales
=
''
this
.
currentPage
=
1
},
handleRowClick
(
row
)
{
this
.
viewGridDetail
(
row
)
},
viewGridDetail
(
grid
)
{
// 模拟获取网格详情
const
installers
=
this
.
installers
.
filter
(
inst
=>
inst
.
gridId
===
grid
.
id
)
const
sales
=
this
.
salesPersons
.
filter
(
sales
=>
sales
.
gridId
===
grid
.
id
)
const
opportunities
=
this
.
opportunities
.
filter
(
opp
=>
opp
.
gridId
===
grid
.
id
)
this
.
selectedGridDetail
=
{
...
grid
,
installers
,
sales
,
opportunities
,
completionRate
:
75
// 模拟成单率
}
this
.
detailDialogVisible
=
true
},
editGrid
(
grid
)
{
this
.
$message
.
info
(
'编辑功能开发中'
)
},
getStatusType
(
status
)
{
const
typeMap
=
{
'assigned'
:
'info'
,
'following'
:
'warning'
,
'pending_review'
:
'primary'
,
'completed'
:
'success'
,
'closed'
:
'info'
}
return
typeMap
[
status
]
||
'info'
},
getStatusLabel
(
status
)
{
const
labelMap
=
{
'assigned'
:
'待跟进'
,
'following'
:
'跟进中'
,
'pending_review'
:
'成单待审核'
,
'completed'
:
'已成单'
,
'closed'
:
'已关闭'
}
return
labelMap
[
status
]
||
status
this
.
selectedGridName
=
''
},
handleSizeChange
(
size
)
{
this
.
pageSize
=
size
...
...
zhiJianBusi/zjbPc/src/views/LoginPage.vue
View file @
e21e465
...
...
@@ -71,32 +71,67 @@
<h1
class=
"login-app-title"
>
上门随销商机管理平台
</h1>
</div>
<el-form
:model=
"loginForm"
:rules=
"loginRules"
ref=
"loginForm"
class=
"login-form"
>
<!-- 登录方式切换 -->
<div
class=
"login-tabs"
>
<div
class=
"login-tab"
:class=
"
{ active: activeTab === 'account' }"
@click="activeTab = 'account'"
>
账号密码登录
</div>
<div
class=
"login-tab"
:class=
"
{ active: activeTab === 'phone' }"
@click="activeTab = 'phone'"
>
手机验证码登录
</div>
</div>
<!-- 账号密码登录表单 -->
<el-form
v-if=
"activeTab === 'account'"
:model=
"loginForm"
:rules=
"loginRules"
ref=
"loginForm"
class=
"login-form"
>
<el-form-item
prop=
"username"
>
<el-input
v-model=
"loginForm.username"
placeholder=
"请输入用户名"
<el-input
v-model=
"loginForm.username"
placeholder=
"请输入用户名"
prefix-icon=
"el-icon-user"
size=
"medium"
@
keyup
.
enter
.
native=
"handleLogin"
/>
</el-form-item>
<el-form-item
prop=
"password"
>
<el-input
v-model=
"loginForm.password"
type=
"password"
placeholder=
"请输入密码"
<el-input
v-model=
"loginForm.password"
type=
"password"
placeholder=
"请输入密码"
prefix-icon=
"el-icon-lock"
size=
"medium"
@
keyup
.
enter
.
native=
"handleLogin"
/>
</el-form-item>
<el-button
type=
"primary"
@
click=
"handleLogin"
:loading=
"loading"
<div
class=
"login-extra-actions"
>
<el-button
type=
"text"
@
click=
"handleForgotPassword"
class=
"forgot-password-btn"
>
忘记密码
</el-button>
</div>
<el-button
type=
"primary"
@
click=
"handleLogin"
:loading=
"loading"
class=
"login-btn"
size=
"medium"
style=
"width: 100%;"
...
...
@@ -106,6 +141,99 @@
</el-button>
</el-form>
<!-- 手机验证码登录表单 -->
<el-form
v-if=
"activeTab === 'phone'"
:model=
"phoneForm"
:rules=
"phoneRules"
ref=
"phoneForm"
class=
"login-form"
>
<el-form-item
prop=
"phone"
>
<el-input
v-model=
"phoneForm.phone"
placeholder=
"请输入手机号"
prefix-icon=
"el-icon-mobile-phone"
size=
"medium"
maxlength=
"11"
@
keyup
.
enter
.
native=
"handlePhoneLogin"
/>
</el-form-item>
<el-form-item
prop=
"captcha"
>
<div
class=
"captcha-wrapper"
>
<el-input
v-model=
"phoneForm.captcha"
placeholder=
"请输入图形验证码"
prefix-icon=
"el-icon-picture"
size=
"medium"
class=
"captcha-input"
@
keyup
.
enter
.
native=
"handlePhoneLogin"
/>
<div
class=
"captcha-image"
@
click=
"refreshCaptcha"
>
<img
:src=
"captchaImage"
alt=
"验证码"
/>
</div>
</div>
</el-form-item>
<el-form-item
prop=
"phoneCode"
>
<div
class=
"phone-code-wrapper"
>
<el-input
v-model=
"phoneForm.phoneCode"
placeholder=
"请输入手机验证码"
prefix-icon=
"el-icon-message"
size=
"medium"
class=
"phone-code-input"
@
keyup
.
enter
.
native=
"handlePhoneLogin"
/>
<el-button
:disabled=
"!canSendCode || codeCountdown > 0"
@
click=
"sendPhoneCode"
class=
"send-code-btn"
size=
"medium"
>
{{
codeCountdown
>
0
?
`${codeCountdown
}
s`
:
'获取验证码'
}}
<
/el-button>
<
/div>
<
/el-form-item>
<
el
-
button
type
=
"primary"
@
click
=
"handlePhoneLogin"
:
loading
=
"phoneLoading"
class
=
"login-btn"
size
=
"medium"
style
=
"width: 100%;"
>
<
i
v
-
if
=
"!phoneLoading"
class
=
"el-icon-right"
><
/i>
{{
phoneLoading
?
'登录中...'
:
'登录'
}}
<
/el-button>
<
/el-form>
<!--
忘记密码弹窗
-->
<
el
-
dialog
title
=
"忘记密码"
:
visible
.
sync
=
"isForgotPasswordDialogVisible"
width
=
"400px"
center
>
<
div
class
=
"forgot-password-content"
>
<
div
class
=
"forgot-password-icon"
>
<
i
class
=
"el-icon-question"
><
/i>
<
/div>
<
p
class
=
"forgot-password-message"
>
忘记密码请联系商机管理员重置密码
<
/p>
<!--
<
div
class
=
"forgot-password-tips"
>
<
p
><
i
class
=
"el-icon-phone"
><
/i> 联系电话:400-8888-8888</
p
>
<
p
><
i
class
=
"el-icon-message"
><
/i> 邮箱:admin@shangmensuixiao.com</
p
>
<
/div> -->
<
/div>
<
span
slot
=
"footer"
class
=
"dialog-footer"
>
<
el
-
button
@
click
=
"isForgotPasswordDialogVisible = false"
>
确定
<
/el-button>
<
/span>
<
/el-dialog>
<
div
class
=
"login-demo-info"
>
<
p
class
=
"login-demo-title"
>
测试账号:
<
/p>
<
div
class
=
"login-demo-accounts"
>
...
...
@@ -114,6 +242,14 @@
<
p
>
地市管理员
:
city001
/
123456
<
/p>
<
p
>
省级管理员
:
province001
/
123456
<
/p>
<
/div>
<
p
class
=
"login-demo-title"
style
=
"margin-top: 16px;"
>
手机验证码登录:
<
/p>
<
div
class
=
"login-demo-accounts"
>
<
p
>
手机号
:
13812345678
(
对应网格管理员
)
<
/p>
<
p
>
手机号
:
13887654321
(
对应区县管理员
)
<
/p>
<
p
>
手机号
:
13765432109
(
对应地市管理员
)
<
/p>
<
p
>
手机号
:
13654321098
(
对应省级管理员
)
<
/p>
<
p
style
=
"color: #E6A23C;"
>
验证码统一为
:
123456
<
/p>
<
/div>
<
/div>
<
/div>
<
/div>
...
...
@@ -127,10 +263,16 @@ export default {
name
:
'LoginPage'
,
data
()
{
return
{
activeTab
:
'account'
,
// 当前激活的登录方式
loginForm
:
{
username
:
''
,
password
:
''
}
,
phoneForm
:
{
phone
:
''
,
captcha
:
''
,
phoneCode
:
''
}
,
loginRules
:
{
username
:
[
{
required
:
true
,
message
:
'请输入用户名'
,
trigger
:
'blur'
}
...
...
@@ -140,7 +282,42 @@ export default {
{
min
:
6
,
message
:
'密码长度不能少于6位'
,
trigger
:
'blur'
}
]
}
,
loading
:
false
phoneRules
:
{
phone
:
[
{
required
:
true
,
message
:
'请输入手机号'
,
trigger
:
'blur'
}
,
{
pattern
:
/^1
[
3-9
]\d
{9
}
$/
,
message
:
'请输入正确的手机号'
,
trigger
:
'blur'
}
],
captcha
:
[
{
required
:
true
,
message
:
'请输入图形验证码'
,
trigger
:
'blur'
}
],
phoneCode
:
[
{
required
:
true
,
message
:
'请输入手机验证码'
,
trigger
:
'blur'
}
,
{
pattern
:
/^
\d
{6
}
$/
,
message
:
'验证码为6位数字'
,
trigger
:
'blur'
}
]
}
,
loading
:
false
,
phoneLoading
:
false
,
captchaImage
:
''
,
// 图形验证码图片
captchaText
:
''
,
// 真实的验证码文本(模拟)
codeCountdown
:
0
,
// 倒计时
countdownTimer
:
null
,
isForgotPasswordDialogVisible
:
false
// 忘记密码弹窗状态
}
}
,
computed
:
{
// 是否可以发送验证码
canSendCode
()
{
return
this
.
phoneForm
.
phone
&&
this
.
phoneForm
.
captcha
&&
this
.
phoneForm
.
captcha
===
this
.
captchaText
}
}
,
mounted
()
{
// 初始化验证码
this
.
refreshCaptcha
()
}
,
beforeDestroy
()
{
// 清理倒计时
if
(
this
.
countdownTimer
)
{
clearInterval
(
this
.
countdownTimer
)
}
}
,
methods
:
{
...
...
@@ -149,12 +326,12 @@ export default {
try
{
await
this
.
$refs
.
loginForm
.
validate
()
this
.
loading
=
true
await
this
.
login
({
username
:
this
.
loginForm
.
username
,
password
:
this
.
loginForm
.
password
}
)
this
.
$message
.
success
(
'登录成功'
)
this
.
$router
.
push
(
'/'
)
}
catch
(
error
)
{
...
...
@@ -163,6 +340,118 @@ export default {
}
finally
{
this
.
loading
=
false
}
}
,
// 刷新图形验证码
refreshCaptcha
()
{
// 生成随机验证码(模拟)
const
chars
=
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let
code
=
''
for
(
let
i
=
0
;
i
<
4
;
i
++
)
{
code
+=
chars
.
charAt
(
Math
.
floor
(
Math
.
random
()
*
chars
.
length
))
}
this
.
captchaText
=
code
// 创建canvas生成验证码图片
const
canvas
=
document
.
createElement
(
'canvas'
)
canvas
.
width
=
100
canvas
.
height
=
40
const
ctx
=
canvas
.
getContext
(
'2d'
)
// 背景色
ctx
.
fillStyle
=
'#f0f0f0'
ctx
.
fillRect
(
0
,
0
,
100
,
40
)
// 绘制验证码文字
ctx
.
font
=
'20px Arial'
ctx
.
fillStyle
=
'#333'
ctx
.
textAlign
=
'center'
ctx
.
textBaseline
=
'middle'
for
(
let
i
=
0
;
i
<
code
.
length
;
i
++
)
{
ctx
.
save
()
ctx
.
translate
(
20
+
i
*
20
,
20
)
ctx
.
rotate
((
Math
.
random
()
-
0.5
)
*
0.3
)
ctx
.
fillText
(
code
[
i
],
0
,
0
)
ctx
.
restore
()
}
// 添加干扰线
for
(
let
i
=
0
;
i
<
4
;
i
++
)
{
ctx
.
strokeStyle
=
`rgba(${Math.random() * 255
}
, ${Math.random() * 255
}
, ${Math.random() * 255
}
, 0.3)`
ctx
.
beginPath
()
ctx
.
moveTo
(
Math
.
random
()
*
100
,
Math
.
random
()
*
40
)
ctx
.
lineTo
(
Math
.
random
()
*
100
,
Math
.
random
()
*
40
)
ctx
.
stroke
()
}
this
.
captchaImage
=
canvas
.
toDataURL
()
}
,
// 发送手机验证码
async
sendPhoneCode
()
{
if
(
this
.
codeCountdown
>
0
)
return
try
{
await
this
.
$refs
.
phoneForm
.
validate
([
'phone'
,
'captcha'
])
// 模拟发送验证码
this
.
$message
.
success
(
'验证码已发送到您的手机'
)
// 开始倒计时
this
.
codeCountdown
=
60
this
.
countdownTimer
=
setInterval
(()
=>
{
this
.
codeCountdown
--
if
(
this
.
codeCountdown
<=
0
)
{
clearInterval
(
this
.
countdownTimer
)
this
.
countdownTimer
=
null
}
}
,
1000
)
}
catch
(
error
)
{
if
(
this
.
phoneForm
.
captcha
!==
this
.
captchaText
)
{
this
.
$message
.
error
(
'图形验证码错误'
)
this
.
refreshCaptcha
()
this
.
phoneForm
.
captcha
=
''
}
}
}
,
// 手机验证码登录
async
handlePhoneLogin
()
{
try
{
await
this
.
$refs
.
phoneForm
.
validate
()
this
.
phoneLoading
=
true
// 模拟手机号登录逻辑
// 这里可以添加手机号到用户名的映射逻辑
const
phoneToUserMap
=
{
'13812345678'
:
{
username
:
'grid001'
,
password
:
'123456'
}
,
'13887654321'
:
{
username
:
'county001'
,
password
:
'123456'
}
,
'13765432109'
:
{
username
:
'city001'
,
password
:
'123456'
}
,
'13654321098'
:
{
username
:
'province001'
,
password
:
'123456'
}
}
const
userInfo
=
phoneToUserMap
[
this
.
phoneForm
.
phone
]
if
(
userInfo
&&
this
.
phoneForm
.
phoneCode
===
'123456'
)
{
await
this
.
login
({
username
:
userInfo
.
username
,
password
:
userInfo
.
password
}
)
this
.
$message
.
success
(
'登录成功'
)
this
.
$router
.
push
(
'/'
)
}
else
{
this
.
$message
.
error
(
'手机号或验证码错误'
)
}
}
catch
(
error
)
{
console
.
error
(
'Phone login error:'
,
error
)
this
.
$message
.
error
(
error
.
message
||
'登录失败,请稍后重试'
)
}
finally
{
this
.
phoneLoading
=
false
}
}
,
// 处理忘记密码
handleForgotPassword
()
{
this
.
isForgotPasswordDialogVisible
=
true
}
}
}
...
...
@@ -294,7 +583,6 @@ export default {
display
:
flex
;
align
-
items
:
center
;
justify
-
content
:
center
;
padding
:
40px
;
.
login
-
form
-
wrapper
{
width
:
100
%
;
...
...
@@ -317,8 +605,123 @@ export default {
}
}
.
login
-
tabs
{
display
:
flex
;
margin
-
bottom
:
32
px
;
background
-
color
:
#
f5f7fa
;
border
-
radius
:
8
px
;
padding
:
4
px
;
.
login
-
tab
{
flex
:
1
;
text
-
align
:
center
;
padding
:
12
px
16
px
;
font
-
size
:
14
px
;
font
-
weight
:
500
;
color
:
#
606266
;
cursor
:
pointer
;
border
-
radius
:
6
px
;
transition
:
all
0.3
s
ease
;
user
-
select
:
none
;
&
.
active
{
background
-
color
:
#
409
EFF
;
color
:
#
ffffff
;
box
-
shadow
:
0
2
px
8
px
rgba
(
64
,
158
,
255
,
0.3
);
}
&
:
hover
:
not
(.
active
)
{
color
:
#
409
EFF
;
background
-
color
:
rgba
(
64
,
158
,
255
,
0.1
);
}
}
}
.
login
-
form
{
margin
-
bottom
:
32
px
;
.
login
-
extra
-
actions
{
text
-
align
:
right
;
margin
-
bottom
:
16
px
;
.
forgot
-
password
-
btn
{
padding
:
0
;
font
-
size
:
14
px
;
color
:
#
409
EFF
;
text
-
decoration
:
none
;
transition
:
all
0.3
s
ease
;
&
:
hover
{
color
:
#
66
b1ff
;
text
-
decoration
:
underline
;
}
}
}
.
captcha
-
wrapper
{
display
:
flex
;
gap
:
12
px
;
align
-
items
:
center
;
.
captcha
-
input
{
flex
:
1
;
}
.
captcha
-
image
{
flex
-
shrink
:
0
;
width
:
100
px
;
height
:
40
px
;
border
:
1
px
solid
#
dcdfe6
;
border
-
radius
:
4
px
;
cursor
:
pointer
;
overflow
:
hidden
;
background
-
color
:
#
f5f7fa
;
display
:
flex
;
align
-
items
:
center
;
justify
-
content
:
center
;
transition
:
all
0.3
s
ease
;
&
:
hover
{
border
-
color
:
#
409
EFF
;
}
img
{
width
:
100
%
;
height
:
100
%
;
object
-
fit
:
cover
;
}
}
}
.
phone
-
code
-
wrapper
{
display
:
flex
;
gap
:
12
px
;
align
-
items
:
center
;
.
phone
-
code
-
input
{
flex
:
1
;
}
.
send
-
code
-
btn
{
flex
-
shrink
:
0
;
width
:
120
px
;
background
-
color
:
#
409
EFF
;
border
-
color
:
#
409
EFF
;
color
:
#
ffffff
;
&
:
hover
:
not
(:
disabled
)
{
background
-
color
:
#
66
b1ff
;
border
-
color
:
#
66
b1ff
;
}
&
:
disabled
{
background
-
color
:
#
c0c4cc
;
border
-
color
:
#
c0c4cc
;
color
:
#
ffffff
;
cursor
:
not
-
allowed
;
}
}
}
}
.
login
-
btn
{
...
...
@@ -348,6 +751,57 @@ export default {
}
}
}
// 忘记密码弹窗样式
.
forgot
-
password
-
content
{
text
-
align
:
center
;
padding
:
20
px
0
;
.
forgot
-
password
-
icon
{
margin
-
bottom
:
20
px
;
i
{
font
-
size
:
48
px
;
color
:
#
409
EFF
;
}
}
.
forgot
-
password
-
message
{
font
-
size
:
16
px
;
color
:
#
303133
;
margin
-
bottom
:
24
px
;
line
-
height
:
1.5
;
}
.
forgot
-
password
-
tips
{
background
-
color
:
#
f5f7fa
;
border
-
radius
:
8
px
;
padding
:
16
px
;
text
-
align
:
left
;
p
{
margin
:
8
px
0
;
font
-
size
:
14
px
;
color
:
#
606266
;
display
:
flex
;
align
-
items
:
center
;
gap
:
8
px
;
&
:
first
-
child
{
margin
-
top
:
0
;
}
&
:
last
-
child
{
margin
-
bottom
:
0
;
}
i
{
color
:
#
409
EFF
;
font
-
size
:
16
px
;
}
}
}
}
}
}
}
...
...
zhiJianBusi/zjbPc/src/views/OpportunityManagement.vue
View file @
e21e465
...
...
@@ -287,7 +287,7 @@
:visible
.
sync=
"isCreateDialogOpen"
width=
"500px"
>
<el-form
:model=
"newOpportunity"
:rules=
"opportunityRules"
ref=
"opportunityForm"
label-width=
"1
0
0px"
>
<el-form
:model=
"newOpportunity"
:rules=
"opportunityRules"
ref=
"opportunityForm"
label-width=
"1
2
0px"
>
<el-form-item
label=
"用户账号"
prop=
"customerAccount"
required
>
<el-input
v-model=
"newOpportunity.customerAccount"
placeholder=
"请输入用户账号"
></el-input>
</el-form-item>
...
...
Write
Preview
Markdown
is supported
Attach a file
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 post a comment