Commit e21e4653 by 李宁

1

1 parent 7df3f72d
{
"permissions": {
"allow": [
"Bash(tree src/ -I node_modules)"
"Bash(tree src/ -I node_modules)",
"Bash(npm run serve)"
],
"deny": [],
"ask": []
......
......@@ -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,
......
......@@ -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,
......
......@@ -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,120 +33,29 @@
</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>
</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 class="search-wrapper">
<el-input
v-model="selectedGrid"
placeholder="请输入网格ID"
style="padding-left: 0;"
@keyup.enter.native="handleFilter"
/>
</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>
<!-- 网格列表 -->
<el-card class="grid-list-card">
......@@ -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="pageStore.pageSize"
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
},
selectedGridName: '',
tableData: [],
pageStore:{
currentPage: 1,
pageSize: 20,
detailDialogVisible: false,
selectedGridDetail: null,
activeTab: 'installers'
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
......
......@@ -71,7 +71,32 @@
<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"
......@@ -93,6 +118,16 @@
/>
</el-form-item>
<div class="login-extra-actions">
<el-button
type="text"
@click="handleForgotPassword"
class="forgot-password-btn"
>
忘记密码
</el-button>
</div>
<el-button
type="primary"
@click="handleLogin"
......@@ -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: {
......@@ -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: 32px;
background-color: #f5f7fa;
border-radius: 8px;
padding: 4px;
.login-tab {
flex: 1;
text-align: center;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
color: #606266;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
user-select: none;
&.active {
background-color: #409EFF;
color: #ffffff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
}
&:hover:not(.active) {
color: #409EFF;
background-color: rgba(64, 158, 255, 0.1);
}
}
}
.login-form {
margin-bottom: 32px;
.login-extra-actions {
text-align: right;
margin-bottom: 16px;
.forgot-password-btn {
padding: 0;
font-size: 14px;
color: #409EFF;
text-decoration: none;
transition: all 0.3s ease;
&:hover {
color: #66b1ff;
text-decoration: underline;
}
}
}
.captcha-wrapper {
display: flex;
gap: 12px;
align-items: center;
.captcha-input {
flex: 1;
}
.captcha-image {
flex-shrink: 0;
width: 100px;
height: 40px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
overflow: hidden;
background-color: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:hover {
border-color: #409EFF;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.phone-code-wrapper {
display: flex;
gap: 12px;
align-items: center;
.phone-code-input {
flex: 1;
}
.send-code-btn {
flex-shrink: 0;
width: 120px;
background-color: #409EFF;
border-color: #409EFF;
color: #ffffff;
&:hover:not(:disabled) {
background-color: #66b1ff;
border-color: #66b1ff;
}
&: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: 20px 0;
.forgot-password-icon {
margin-bottom: 20px;
i {
font-size: 48px;
color: #409EFF;
}
}
.forgot-password-message {
font-size: 16px;
color: #303133;
margin-bottom: 24px;
line-height: 1.5;
}
.forgot-password-tips {
background-color: #f5f7fa;
border-radius: 8px;
padding: 16px;
text-align: left;
p {
margin: 8px 0;
font-size: 14px;
color: #606266;
display: flex;
align-items: center;
gap: 8px;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
i {
color: #409EFF;
font-size: 16px;
}
}
}
}
}
}
}
......
......@@ -287,7 +287,7 @@
:visible.sync="isCreateDialogOpen"
width="500px"
>
<el-form :model="newOpportunity" :rules="opportunityRules" ref="opportunityForm" label-width="100px">
<el-form :model="newOpportunity" :rules="opportunityRules" ref="opportunityForm" label-width="120px">
<el-form-item label="用户账号" prop="customerAccount" required>
<el-input v-model="newOpportunity.customerAccount" placeholder="请输入用户账号"></el-input>
</el-form-item>
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!