Commit fc86c70c by 李宁

1

1 parent da7b0e06
Showing 43 changed files with 3232 additions and 2191 deletions
import request from '../../request'
/**
*
/**
* 查询账号列表
*/
export function queryAllMissionRecord() {
export function queryAccountList(data) {
return request({
url: 'hallserver/companyJob/queryAllCompanyJob',
data: {}
url: '/compass/api/system/account/page',
data,
})
}
/**
* 查询角色列表
*/
export function queryRoleList(data) {
return request({
url: '/compass/api/common/enums/account-roles',
method: 'GET'
})
}
/**
* 删除账号
*/
export function deleteRole(data) {
return request({
url: '/compass/api/system/account/delete',
data,
})
}
/**
* 添加/更新账号
*/
export function addAndUpdateRole(data) {
return request({
url: '/compass/api/system/account/create' + (data.id?'/update':''),
data,
})
}
\ No newline at end of file
......@@ -11,6 +11,76 @@ export function queryAllBusi(data) {
}
/**
* 全部商机-商机审核
*/
export function audioBusi(data) {
return request({
url: '/compass/api/opportunity/audit',
data,
})
}
/**
* 全部商机-商机详情
*/
export function queryBusiDetail(data) {
return request({
url: '/compass/api/opportunity/detail',
data,
})
}
/**
* 全部商机-商机跟进记录查询
*/
export function queryBusiFollowList(data) {
return request({
url: '/compass/api/opportunity/follow/page',
data,
})
}
/**
* 全部商机-关闭商机
*/
export function closeBusi(data) {
return request({
url: '/compass/api/opportunity/close',
data,
})
}
/**
* 全部商机-标记成单
*/
export function dealBusi(data) {
return request({
url: '/compass/api/opportunity/deal',
data,
})
}
/**
* 全部商机-分配商机
*/
export function reassignBusi(data) {
return request({
url: '/compass/api/opportunity/reassign',
data,
})
}
/**
* 全部商机-记录管理员备注
*/
export function updateRemark(data) {
return request({
url: '/compass/api/opportunity/admin-remark',
data,
})
}
/**
* 全部商机-数据统计
*/
export function queryAllBusiStatistics(data) {
......@@ -21,11 +91,81 @@ export function queryAllBusiStatistics(data) {
}
/**
* 全部商机-查询商机标签列表
* 全部商机-查询所有商机标签列表
*/
export function queryBusiLabel(data) {
export function queryBusiLabelList(data) {
return request({
url: '/compass/api/opportunity/tag/page',
data,
})
}
/**
* 商机标签的开始和关闭
*/
export function updateBusiLabelStatus(data) {
return request({
url: '/compass/api/opportunity/tag/status',
data,
})
}
/**
* 商机标签的删除
*/
export function deleteBusiLabel(data) {
return request({
url: '/compass/api/opportunity/tag/delete',
data,
})
}
/**
* 商机标签的创建和更新
*/
export function createAndUpdateTag(data) {
return request({
url: '/compass/api/opportunity/tag' + (data.id?'/update':''),
data,
})
}
/**
* 商机关闭原因列表查询
*/
export function queryBusiCloseReansonList(data) {
return request({
url: '/compass/api/opportunity/close-reason/page',
data,
})
}
/**
* 商机关闭原因添加和更新
*/
export function busiCloseReasonUpdate(data) {
return request({
url: '/compass/api/opportunity/close-reason'+(data.id?'/update':''),
data,
})
}
/**
* 商机关闭原因删除
*/
export function busiCloseReasonDel(data) {
return request({
url: '/compass/api/opportunity/close-reason/delete',
data,
})
}
/**
* 全部商机-新增商机
*/
export function createBusi(data) {
return request({
url: '/compass/api/opportunity/create',
data,
})
}
\ No newline at end of file
import request from '../../request'
/**
* 查询区域列表
* 根据级别查询区域列表
* 1-省级,2-市级,3-区级,4-网格级
*/
export function queryAllArea(data) {
export function queryLevelAllArea(data) {
return request({
url: '/compass/api/common/areas/level?areaLevel='+data.areaLevel+'&parentAreaCode='+data.parentAreaCode,
method: 'GET'
......
import request from '../../request'
/**
* 资金管理-发票管理-开票订单查询
* 网格列表查询
* @returns {AxiosPromise}
*/
export function queryTicketOrderList() {
export function queryAllGridList(data) {
return request({
url: 'hallserver/v1/invoiceApply/queryCompanyOrder',
data: {}
url: '/compass/api/grid/list',
data,
})
}
/**
* 获取该账号下可选的网格列表
*/
export function queryGridList(data) {
return request({
url: '/compass/api/grid/options',
data,
})
}
\ No newline at end of file
import request from '../../request'
/**
* 查询人员列表
*/
export function queryAllPerson(data) {
return request({
url: '/compass/api/personnel/list',
data,
})
}
/**
* 添加人员
*/
export function addNewPerson(data) {
return request({
url: '/compass/api/personnel/create',
data,
})
}
/**
*
* 删除人员
*/
export function companyInfoQuery(companyId) {
export function deletePerson(data) {
return request({
url: 'hallserver/hallCompanyInfo/getCompanyInfo',
data: {
'companyId': companyId
}
url: '/compass/api/personnel/delete',
data,
})
}
/**
* 批量导入工维人员
*/
export function importGwPerson(data) {
return request({
url: '/compass/api/personnel/import-maintenance-preview',
data,
})
}
/**
* 批量导入营销人员
*/
export function importYxPerson(data) {
return request({
url: '/compass/api/marketing/import-preview',
data,
})
}
......@@ -5,7 +5,7 @@ import request from './request'
*/
export function logout() {
return request({
url: 'hallserver/admin/logout',
url: '/compass/api/auth/logout',
data: {}
})
}
......@@ -23,24 +23,36 @@ export function login(data) {
}
/**
* 获取登录验证码
* 手机账号登录
*/
export function pohoneLogin(data) {
return request({
url: '/compass/api/auth/phone-login',
data,
})
}
/**
* 获取图形验证码
* @param loginName
* @param password
*/
export function loginCode(data) {
export function getImgCode(data) {
return request({
url: 'hallserver/admin/loginCode',
url: '/compass/api/auth/getCaptCha',
method: 'GET',
data,
})
}
/**
* 获取路由权限
* @returns {AxiosPromise}
* 获取短信验证码
* @param loginName
* @param password
*/
export function getMenus() {
export function getTelCode(data) {
return request({
url: 'hallserver/admin/function',
data: {}
url: '/compass/api/auth/send-sms',
data,
})
}
......@@ -51,7 +51,7 @@ let catchFun = function (msg) {
service.interceptors.response.use(
(response) => {
console.log(response);
if (response.status === 200) {
if (response.status == 200) {
if (response.data.code == "401") {
//登陆失效,重新登陆
catchFun("账户状态异常");
......@@ -100,9 +100,9 @@ service.interceptors.response.use(
};
}
}
} else if (response.status === 302) {
} else if (response.status == 302) {
catchFun("登陆失效,请重新登陆");
} else if (response.status === 401 || response.status == 403) {
} else if (response.status == 401 || response.status == 403) {
catchFun("账户状态异常");
} else {
if (sessionStorage.notFirstIn) {
......
......@@ -212,9 +212,9 @@ export default {
* @returns
*/
hideIdNumber: function(idCard) {
if (idCard.length === 18) {
if (idCard.length == 18) {
return idCard.substr(0, 6) + '********' + idCard.substr(-4, 4)
} else if (idCard.length === 15) {
} else if (idCard.length == 15) {
return idCard.substr(0, 6) + '******' + idCard.substr(-3, 3)
} else {
return idCard
......
......@@ -2,45 +2,35 @@
<div class="account-management">
<div class="toolbar">
<p class="description">管理系统登录账号</p>
<el-button @click="isAddDialogOpen = true" type="primary" size="small">
<el-button @click="addAccount" type="primary" size="small">
<i class="el-icon-plus"></i>
添加账号
</el-button>
</div>
<div class="account-table">
<el-table :data="accounts" style="width: 100%" border>
<el-table-column prop="username" label="用户名" width="120"></el-table-column>
<el-table-column prop="name" label="姓名" width="100"></el-table-column>
<el-table-column prop="role" label="角色" width="120">
<template slot-scope="scope">
<el-tag size="mini">{{ scope.row.role }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="region" label="所属区域" width="220"></el-table-column>
<el-table-column label="联系方式" width="150">
<template slot-scope="scope">
<span>{{ scope.row.phone || scope.row.email || '-' }}</span>
</template>
</el-table-column>
<el-table :data="tableData" style="width: 100%" border>
<el-table-column prop="account" label="用户名" width="180"></el-table-column>
<el-table-column prop="accountName" label="姓名" width="150"></el-table-column>
<el-table-column prop="roleName" label="角色" width="120"></el-table-column>
<el-table-column prop="areaName" label="所属区域" width="220"></el-table-column>
<el-table-column prop="contactPhone" label="手机号" width="220"></el-table-column>
<el-table-column prop="email" label="邮箱" width="250"></el-table-column>
<el-table-column label="状态" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
<el-tag :type="scope.row.status == '1' ? 'success' : 'info'">
{{ scope.row.status == '1' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastLogin" label="最近登录" width="220">
<template slot-scope="scope">
<span>{{ scope.row.lastLogin || '从未登录' }}</span>
</template>
</el-table-column>
<el-table-column label="操作">
<el-table-column prop="lastLoginTime" label="最近登录" width="220" :formatter="timeRender"></el-table-column>
<el-table-column label="操作" min-width="150">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="editAccount(scope.row)"
disabled
>
编辑
</el-button>
......@@ -56,28 +46,41 @@
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageStore.currentPage"
:page-sizes="[20, 50, 100]"
:page-size="pageStore.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageStore.total"
>
</el-pagination>
</div>
<!-- 添加/编辑账号对话框 -->
<el-dialog
:title="editingAccount ? '编辑账号' : '添加账号'"
:visible.sync="isAddDialogOpen"
width="800px"
>
width="800px">
<el-form :model="accountForm" :rules="accountRules" ref="accountForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用户名" prop="username">
<el-form-item label="用户名" prop="account">
<el-input
v-model="accountForm.username"
v-model="accountForm.account"
placeholder="请输入用户名"
:disabled="!!editingAccount"
></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-form-item label="密码" prop="password">
<el-input
v-model="accountForm.name"
placeholder="请输入姓名"
v-model="accountForm.password"
placeholder="请输入密码"
></el-input>
</el-form-item>
</el-col>
......@@ -85,20 +88,17 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="角色" prop="role">
<el-select v-model="accountForm.role" style="width: 100%;">
<el-option label="网格管理员" value="网格管理员"></el-option>
<el-option label="区县管理员" value="区县管理员"></el-option>
<el-option label="地市管理员" value="地市管理员"></el-option>
<el-option label="省级管理员" value="省级管理员"></el-option>
</el-select>
<el-form-item label="姓名" prop="accountName">
<el-input
v-model="accountForm.accountName"
placeholder="请输入姓名"
></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="region">
<el-select v-model="accountForm.status">
<el-option label="启用" value="active"></el-option>
<el-option label="禁用" value="inactive"></el-option>
<el-form-item label="角色" prop="roleCode">
<el-select v-model="accountForm.roleCode" style="width: 100%;">
<el-option v-for="item in roleList" :label="item.description" :value="item.key" :disabled="ifRoleCho(item.key)"></el-option>
</el-select>
</el-form-item>
</el-col>
......@@ -106,13 +106,23 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-form-item label="状态" prop="status">
<el-select v-model="accountForm.status">
<el-option label="启用" value="1"></el-option>
<el-option label="禁用" value="0"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号" prop="contactPhone">
<el-input
v-model="accountForm.phone"
v-model="accountForm.contactPhone"
placeholder="请输入手机号"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input
......@@ -124,9 +134,9 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-row :gutter="20" v-if="accountForm.roleCode">
<el-col :span="30">
<el-form-item label="所属区域" prop="region">
<el-form-item label="所属区域" prop="city">
<template v-if="accountForm.cityArr.length>0">
<el-select
v-model="accountForm.city"
......@@ -137,13 +147,13 @@
clearable>
<el-option
v-for="item in accountForm.cityArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="accountForm.countyArr.length>0">
<template v-if="accountForm.countyArr.length>0 && accountForm.roleCode!='CITY'">
<el-select
v-model="accountForm.county"
placeholder="选择区县"
......@@ -153,13 +163,13 @@
clearable>
<el-option
v-for="item in accountForm.countyArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="accountForm.gridArr.length>0">
<template v-if="accountForm.gridArr.length>0 && accountForm.roleCode!='CITY' && accountForm.roleCode!='COUNTY'">
<el-select
v-model="accountForm.grid"
placeholder="选择网格"
......@@ -167,9 +177,9 @@
clearable>
<el-option
v-for="item in accountForm.gridArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.gridCode"
:label="item.gridName"
:value="item.gridCode"
></el-option>
</el-select>
</template>
......@@ -187,85 +197,152 @@
</template>
<script>
// 模拟账号数据
const mockAccounts = [
{ id: 'ACC001', username: 'grid001', name: '李网格长', role: '网格管理员', region: '南京市玄武区A网格', phone: '13543210987', status: 'active', lastLogin: '2025-09-28 15:30:00', createTime: '2025-09-01 09:00:00' },
{ id: 'ACC002', username: 'county001', name: '王区长', role: '区县管理员', region: '南京市玄武区', phone: '13432109876', status: 'active', lastLogin: '2025-09-28 14:20:00', createTime: '2025-09-01 09:00:00' },
{ id: 'ACC003', username: 'city001', name: '张市长', role: '地市管理员', region: '南京市', phone: '13321098765', status: 'active', lastLogin: '2025-09-28 10:15:00', createTime: '2025-09-01 09:00:00' }
]
export default {
name: 'AccountManagement',
data() {
return {
accounts: mockAccounts,
isAddDialogOpen: false,
editingAccount: null,
tableData: [],
pageStore:{
currentPage: 1,
pageSize: 20,
total: 0
},
accountForm: {
username: '',
name: '',
role: '网格管理员',
region: '',
phone: '',
account: '',
password: '',
accountName: '',
roleCode: '',
status: '',
grid: '',
contactPhone: '',
email: '',
city: '',
cityArr: this.addressStoreData,
cityArr: [],
county: '',
countyArr: [],
grid: '',
gridArr: [],
status: 'active'
gridArr: []
},
getData:{
city: '',
county: '',
grid: '',
},
roleList: [],
accountRules: {
username: [
account: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
name: [
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
],
accountName: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
region: [
city: [
{ required: true, message: '请输入所属区域', trigger: 'blur' }
]
],
roleCode: [
{ required: true, message: '请选择角色', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'blur' }
],
},
getData:{
city: '',
cityName: '',
county: '',
countyName: '',
grid: '',
gridName: '',
},
isAddDialogOpen: false,
editingAccount: null,
}
},
created(){
this.setAddressShow()
let pa = JSON.parse(localStorage.getItem('accountInfo'))
this.getData.city = pa.cityCode||''
this.getData.county = pa.countyCode||''
this.getData.grid = pa.gridCode||''
this.queryArea()
this.handleFilter()
this.queryRole()
},
methods: {
ifRoleCho(key){
let gd = this.getData
if(gd.county){
if(key == 'GRID'){
return false
}
return true
}
if(gd.city){
if(key=='GRID' || key=='COUNTY'){
return false
}
return true
}
if(key != 'PROVINCE'){
return false
}
return true
},
queryRole(){
this.apiReq.queryRoleList().then(res=>{
if(res.code == 200){
this.roleList = res.data
}
})
},
timeRender(row,column){
return this.common.detailTime(new Date(row[column.property]).getTime())
},
handleFilter(){
this.pageStore.currentPage = 1
this.queryAccount()
},
queryAccount(){
this.apiReq.queryAccountList({
pageNum: this.pageStore.currentPage,
pageSize: this.pageStore.pageSize
}).then(res=>{
if(res.code == 200){
this.tableData = res.data.records
this.pageStore.total = res.data.total
}else{
}
})
},
queryArea(){
this.apiReq.queryLevelAllArea({
areaLevel: 2,
parentAreaCode: '320000'
}).then(res=>{
if(res.code == 200){
this.accountForm.cityArr = res.data
this.setAddressShow()
}
})
},
setAddressShow(){
let ad = this.accountForm
let gd = this.getData
if(gd.city){
ad.city = gd.city
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
}
})
this.cityChange(ad.city)
if(gd.county){
ad.county = gd.county
ad.countyArr.forEach(item=>{
if(item.value == ad.county){
ad.gridArr = item.children
}
})
if(ad.gridArr.length == 1){
gd.grid = 'moren'
}
this.countyChange(ad.county)
if(gd.grid){
ad.grid = gd.grid
......@@ -279,9 +356,12 @@ export default {
ad.county = ''
ad.grid = ''
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
this.apiReq.queryLevelAllArea({
areaLevel: 3,
parentAreaCode: value
}).then(res=>{
if(res.code == 200){
ad.countyArr = res.data
}
})
},
......@@ -290,14 +370,55 @@ export default {
ad.county = value
ad.grid = ''
ad.countyArr.forEach(item=>{
if(item.value == ad.county){
ad.gridArr = item.children
this.apiReq.queryGridList({
areaCode:value
}).then(res=>{
if(res.code == 200){
ad.gridArr = res.data
}
})
},
handleSizeChange(size) {
this.pageStore.pageSize = size
this.handleFilter()
},
handleCurrentChange(page) {
this.pageStore.currentPage = page
this.queryAccount()
},
addAccount(){
let ad = this.accountForm
this.editingAccount = null
ad = {
account: '',
password: '',
accountName: '',
roleCode: '',
status: '',
contactPhone: '',
email: ''
}
if(!this.getData.city){
ad.city = ''
ad.county = ''
ad.countyArr = []
ad.grid = ''
ad.gridArr = []
}else if(!this.getData.county){
ad.county = ''
ad.grid = ''
ad.gridArr = []
}else if(!this.getData.grid){
ad.grid = ''
}
this.isAddDialogOpen = true
},
editAccount(account) {
debugger
this.editingAccount = account
this.accountForm = { ...account }
this.isAddDialogOpen = true
......@@ -308,8 +429,21 @@ export default {
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.accounts = this.accounts.filter(a => a.id !== id)
this.$message.success('账号删除成功')
this.apiReq.deleteRole({
accountId: id
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('删除成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
}
});
}else{
this.$message.error(res.message)
}
})
}).catch(() => {
// 用户取消删除
})
......@@ -317,41 +451,44 @@ export default {
submitAccountForm() {
this.$refs.accountForm.validate((valid) => {
if (valid) {
if (this.editingAccount) {
// 更新账号信息
const index = this.accounts.findIndex(a => a.id === this.editingAccount.id)
if (index !== -1) {
this.$set(this.accounts, index, { ...this.editingAccount, ...this.accountForm })
this.$message.success('账号信息更新成功')
}
} else {
// 添加新账号
const newAccount = {
...this.accountForm,
id: `ACC${Date.now()}`,
createTime: new Date().toLocaleString('zh-CN')
}
this.accounts.push(newAccount)
this.$message.success('账号添加成功')
let ud = this.accountForm
if(ud.roleCode=='GRID' && !ud.grid){
this.$message.error('请补全区域信息')
return
}
if(ud.roleCode=='COUNTY' && !ud.county){
this.$message.error('请补全区域信息')
return
}
// 重置表单和状态
this.resetAccountForm()
this.isAddDialogOpen = false
this.editingAccount = null
this.apiReq.addAndUpdateRole({
id: this.editingAccount?this.editingAccount.id:'',
account: ud.account,
password: ud.password,
accountName: ud.accountName,
roleCode: ud.roleCode,
status: ud.status,
contactPhone: ud.contactPhone,
email: ud.email,
areaCode: ud.roleCode=='CITY'?ud.city:ud.county,
gridCode: ud.roleCode=='CITY'||ud.roleCode=='COUNTY'?'':ud.grid
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('添加成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
__this.isAddDialogOpen = false
}
});
}else{
this.$message.error(res.message)
}
})
}
})
},
resetAccountForm() {
this.accountForm = {
username: '',
name: '',
role: '网格管理员',
region: '',
phone: '',
email: '',
status: 'active'
}
}
}
}
......
......@@ -2,21 +2,21 @@
<div class="close-reason-management">
<div class="toolbar">
<p class="description">管理商机关闭时可选择的原因</p>
<el-button @click="isAddDialogOpen = true" type="primary" size="small">
<el-button @click="addReason" type="primary" size="small">
<i class="el-icon-plus"></i>
添加原因
</el-button>
</div>
<div class="reason-table">
<el-table :data="reasons" style="width: 100%">
<el-table-column prop="reason" label="关闭原因" width="300"></el-table-column>
<el-table :data="reasonsList" style="width: 100%" border>
<el-table-column prop="closeReason" label="关闭原因" width="300"></el-table-column>
<el-table-column prop="category" label="分类" width="200">
<template slot-scope="scope">
<el-tag size="mini">{{ scope.row.category }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<el-table-column label="操作" min-width="150">
<template slot-scope="scope">
<el-button
type="text"
......@@ -37,6 +37,20 @@
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageStore.currentPage"
:page-sizes="[20, 50, 100]"
:page-size="pageStore.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageStore.total"
>
</el-pagination>
</div>
<!-- 添加/编辑原因对话框 -->
<el-dialog
:title="editingReason ? '编辑关闭原因' : '添加关闭原因'"
......@@ -44,9 +58,9 @@
width="500px"
>
<el-form :model="reasonForm" :rules="reasonRules" ref="reasonForm" label-width="100px">
<el-form-item label="关闭原因" prop="reason">
<el-form-item label="关闭原因" prop="closeReason">
<el-input
v-model="reasonForm.reason"
v-model="reasonForm.closeReason"
placeholder="请输入关闭原因"
></el-input>
</el-form-item>
......@@ -68,21 +82,18 @@
</template>
<script>
// 模拟关闭原因数据
const mockCloseReasons = [
{ id: '1', reason: '客户价格不接受', category: '价格因素' },
{ id: '2', reason: '客户暂无需求', category: '需求因素' },
{ id: '3', reason: '竞品已安装', category: '竞争因素' },
{ id: '4', reason: '客户联系不上', category: '联系因素' },
{ id: '5', reason: '技术不支持', category: '技术因素' },
{ id: '6', reason: '其他原因', category: '其他' }
]
export default {
name: 'CloseReasonManagement',
data() {
return {
reasons: mockCloseReasons,
reasonsList: [],
pageStore:{
currentPage: 1,
pageSize: 20,
total: 0
},
isAddDialogOpen: false,
editingReason: null,
reasonForm: {
......@@ -90,7 +101,7 @@ export default {
category: ''
},
reasonRules: {
reason: [
closeReason: [
{ required: true, message: '请输入关闭原因', trigger: 'blur' }
],
category: [
......@@ -99,7 +110,46 @@ export default {
}
}
},
created(){
this.handleFilter()
},
methods: {
handleSizeChange(size) {
this.pageStore.pageSize = size
this.handleFilter()
},
handleCurrentChange(page) {
this.pageStore.currentPage = page
this.queryCloseList()
},
handleFilter(){
this.pageStore.currentPage = 1
this.queryCloseList()
},
queryCloseList(){
this.apiReq.queryBusiCloseReansonList({
pageNum: this.pageStore.currentPage,
pageSize: this.pageStore.pageSize
}).then(res=>{
if(res.code == 200){
this.reasonsList = res.data.records
this.pageStore.total = res.data.total
}
})
},
addReason(){
this.editingReason = null
this.reasonForm = {
category: '',
closeReason: ''
}
this.isAddDialogOpen = true
},
editReason(reason) {
this.editingReason = reason
this.reasonForm = { ...reason }
......@@ -111,44 +161,46 @@ export default {
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.reasons = this.reasons.filter(r => r.id !== id)
this.$message.success('关闭原因删除成功')
}).catch(() => {
// 用户取消删除
this.apiReq.busiCloseReasonDel({
id,
}).then(res=>{
if(res.code == 200){
let __this = this
this.$alert('删除成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
}
});
}else{
this.$message.error(res.message)
}
})
})
},
submitReasonForm() {
this.$refs.reasonForm.validate((valid) => {
if (valid) {
if (this.editingReason) {
// 更新原因信息
const index = this.reasons.findIndex(r => r.id === this.editingReason.id)
if (index !== -1) {
this.$set(this.reasons, index, { ...this.editingReason, ...this.reasonForm })
this.$message.success('关闭原因更新成功')
this.apiReq.busiCloseReasonUpdate({
id: this.editingReason?this.editingReason.id: '',
category: this.reasonForm.category,
closeReason: this.reasonForm.closeReason
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert(this.editingReason?'修改成功':'添加成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
__this.isAddDialogOpen = false
}
});
}else{
this.$message.error(res.message)
}
} else {
// 添加新原因
const newReason = {
...this.reasonForm,
id: `REASON${Date.now()}`
}
this.reasons.push(newReason)
this.$message.success('关闭原因添加成功')
}
// 重置表单和状态
this.resetReasonForm()
this.isAddDialogOpen = false
this.editingReason = null
})
}
})
},
resetReasonForm() {
this.reasonForm = {
reason: '',
category: ''
}
}
}
}
......
......@@ -5,43 +5,60 @@
<div class="toolbar-left">
<div class="search-wrapper">
<el-input
v-model="searchQuery"
v-model="formData.personnelCode"
placeholder="请输入工号查询"
class="search-input"
/>
</div>
<div class="search-wrapper">
<el-input
v-model="searchPhone"
v-model="formData.phone"
placeholder="请输入手机号查询"
class="search-input"
/>
</div>
<el-cascader
v-if="hasProvincePermission"
v-model="selectedRegion"
:options="regionOptions"
:props="{ checkStrictly: true, value: 'name', label: 'name' }"
placeholder="所属区域筛选"
clearable
class="region-cascader"
></el-cascader>
<el-select
v-else
v-model="selectedRegionDistrict"
placeholder="所属区域筛选"
clearable
class="region-select"
>
<el-option label="玄武区" value="玄武区"></el-option>
<el-option label="秦淮区" value="秦淮区"></el-option>
<el-option label="建邺区" value="建邺区"></el-option>
<el-option label="鼓楼区" value="鼓楼区"></el-option>
</el-select>
<div class="search-wrapper" style="width: auto;">
<template v-if="addressStore.cityArr.length>0">
<el-select
v-model="addressStore.city"
class="filter-select"
placeholder="选择地市"
@change="cityChange"
:disabled="getData.city!=''"
style="margin-right: 20px;"
clearable>
<el-option
v-for="item in addressStore.cityArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="addressStore.countyArr.length>0">
<el-select
v-model="addressStore.county"
class="filter-select"
placeholder="选择区县"
:disabled="getData.county!=''"
clearable>
<el-option
v-for="item in addressStore.countyArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
</div>
</div>
<div class="toolbar-right">
<el-button @click="isAddDialogOpen = true" type="primary" size="small">
<el-button size="small" @click="handleFilter">
<i class="el-icon-search"></i>
查询
</el-button>
<el-button @click="updatePerson" type="primary" size="small">
<i class="el-icon-plus"></i>
添加人员
</el-button>
......@@ -50,25 +67,26 @@
<!-- 表格 -->
<div class="personnel-table">
<el-table :data="paginatedPersonnel" border>
<el-table-column prop="name" label="姓名" width="100"></el-table-column>
<el-table-column prop="workId" label="工号" width="100"></el-table-column>
<el-table :data="tableData" border>
<el-table-column prop="personnelName" label="姓名" width="100"></el-table-column>
<el-table-column prop="personnelCode" label="工号" width="100"></el-table-column>
<el-table-column prop="phone" label="手机号" width="150"></el-table-column>
<el-table-column prop="region" label="所属区域" width="250"></el-table-column>
<el-table-column prop="areaName" label="所属区域" width="250"></el-table-column>
<el-table-column label="状态" width="120">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'enabled' ? 'success' : 'info'">
{{ scope.row.status === 'enabled' ? '启用' : '关闭' }}
<el-tag :type="scope.row.status == '1' ? 'success' : 'info'">
{{ scope.row.status == '1' ? '启用' : '关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="220"></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="220" :formatter="timeRender"></el-table-column>
<el-table-column label="操作" min-width="120">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="editPersonnel(scope.row)"
disabled
>
编辑
</el-button>
......@@ -89,35 +107,34 @@
<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="filteredPersonnel.length"
:total="pageStore.total"
>
</el-pagination>
</div>
<!-- 添加/编辑人员对话框 -->
<el-dialog
:title="editingPersonnel ? '编辑政企业务营销人员' : '新增政企业务营销人员'"
:visible.sync="isAddDialogOpen"
width="600px"
>
<el-form :model="personnelForm" :rules="personnelRules" ref="personnelForm" label-width="100px">
:title="editingPersonnel ? '编辑政企业务支撑人员' : '新增政企业务支撑人员'"
:visible.sync="updatePersonStore.isShow"
width="800px">
<el-form :model="updatePersonStore" :rules="personnelRules" ref="personnelForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-form-item label="姓名" prop="personnelName">
<el-input
v-model="personnelForm.name"
v-model="updatePersonStore.personnelName"
placeholder="请输入姓名"
></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工号" prop="workId">
<el-form-item label="工号" prop="personnelCode">
<el-input
v-model="personnelForm.workId"
v-model="updatePersonStore.personnelCode"
placeholder="请输入工号"
:disabled="!!editingPersonnel"
></el-input>
......@@ -130,33 +147,65 @@
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input
v-model="personnelForm.phone"
v-model="updatePersonStore.phone"
placeholder="请输入手机号"
></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="updatePersonStore.status" style="width: 100%;">
<el-option label="启用" value="1"></el-option>
<el-option label="关闭" value="0"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="30">
<el-form-item label="所属区域" prop="grid">
<template v-if="updatePersonStore.cityArr.length>0">
<el-select
v-model="updatePersonStore.city"
placeholder="选择地市"
@change="(value)=>{cityChange(value,'person')}"
:disabled="getData.city!=''"
style="margin-right: 10px"
clearable>
<el-option
v-for="item in updatePersonStore.cityArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="updatePersonStore.countyArr.length>0">
<el-select
v-model="updatePersonStore.county"
placeholder="选择区县"
@change="(value)=>{countyChange(value,'person')}"
:disabled="getData.county!=''"
style="margin-right: 10px"
clearable>
<el-option
v-for="item in updatePersonStore.countyArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="所属区域" prop="region">
<el-cascader
v-model="personnelForm.region"
:options="regionOptions"
:props="{ checkStrictly: true, value: 'name', label: 'name' }"
placeholder="请选择区域"
style="width: 100%;"
></el-cascader>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="personnelForm.status" style="width: 100%;">
<el-option label="启用" value="enabled"></el-option>
<el-option label="关闭" value="disabled"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isAddDialogOpen = false">取消</el-button>
<el-button @click="updatePersonStore.isShow = false">取消</el-button>
<el-button type="primary" @click="submitPersonnelForm">确定</el-button>
</div>
</el-dialog>
......@@ -164,128 +213,204 @@
</template>
<script>
import { mapGetters } from 'vuex'
// 模拟政企业务营销人员数据
const mockEnterprisePersonnel = [
{ id: 'EP001', name: '刘政企', phone: '13912345678', workId: 'E001', type: '装维师傅', region: '江苏省-南京市-玄武区', gridName: 'A网格', associatedSales: 'SALE001', status: 'enabled', createTime: '2025-09-01 09:00:00' },
{ id: 'EP002', name: '赵政企', phone: '13923456789', workId: 'E002', type: '装维师傅', region: '江苏省-南京市-秦淮区', gridName: 'B网格', associatedSales: 'SALE002', status: 'enabled', createTime: '2025-09-02 10:00:00' },
{ id: 'EP003', name: '孙政企', phone: '13934567890', workId: 'E003', type: '装维师傅', region: '江苏省-南京市-建邺区', gridName: 'C网格', associatedSales: 'SALE003', status: 'disabled', createTime: '2025-09-03 11:00:00' },
{ id: 'EP004', name: '周政企', phone: '13945678901', workId: 'E004', type: '装维师傅', region: '江苏省-南京市-鼓楼区', gridName: 'D网格', associatedSales: 'SALE004', status: 'enabled', createTime: '2025-09-04 12:00:00' },
{ id: 'EP005', name: '吴政企', phone: '13956789012', workId: 'E005', type: '装维师傅', region: '江苏省-南京市-浦口区', gridName: 'E网格', associatedSales: 'SALE005', status: 'enabled', createTime: '2025-09-05 13:00:00' },
{ id: 'EP006', name: '郑政企', phone: '13967890123', workId: 'E006', type: '装维师傅', region: '江苏省-南通市-崇川区', gridName: 'F网格', associatedSales: 'SALE006', status: 'enabled', createTime: '2025-09-06 14:00:00' },
{ id: 'EP007', name: '王政企', phone: '13978901234', workId: 'E007', region: '江苏省-南通市-港闸区', status: 'disabled', createTime: '2025-09-07 15:00:00' },
{ id: 'EP008', name: '冯政企', phone: '13989012345', workId: 'E008', region: '江苏省-苏州市-姑苏区', status: 'enabled', createTime: '2025-09-08 16:00:00' },
{ id: 'EP009', name: '陈政企', phone: '13990123456', workId: 'E009', region: '江苏省-苏州市-虎丘区', status: 'enabled', createTime: '2025-09-09 17:00:00' },
{ id: 'EP010', name: '褚政企', phone: '13901234567', workId: 'E010', region: '江苏省-南京市-栖霞区', status: 'enabled', createTime: '2025-09-10 18:00:00' }
]
// 地区数据
const regionOptions = [
{
name: '江苏省',
children: [
{
name: '南京市',
children: [
{ name: '玄武区' },
{ name: '秦淮区' },
{ name: '建邺区' },
{ name: '鼓楼区' },
{ name: '浦口区' },
{ name: '栖霞区' },
{ name: '雨花台区' },
{ name: '江宁区' },
{ name: '六合区' },
{ name: '溧水区' },
{ name: '高淳区' }
]
},
{
name: '南通市',
children: [
{ name: '崇川区' },
{ name: '港闸区' }
]
},
{
name: '苏州市',
children: [
{ name: '姑苏区' },
{ name: '虎丘区' }
]
}
]
}
]
export default {
name: 'EnterprisePersonnelManagement',
data() {
return {
personnel: mockEnterprisePersonnel,
searchQuery: '',
searchPhone: '',
selectedRegion: [],
selectedRegionDistrict: '',
isAddDialogOpen: false,
editingPersonnel: null,
currentPage: 1,
pageSize: 20,
personnelForm: {
name: '',
workId: '',
formData:{
personnelCode: '',
phone: '',
region: [],
status: 'enabled'
personnelTypes: [3],
},
addressStore:{
city: '',
cityArr: [],
county: '',
countyArr: []
},
getData:{
city: '',
county: ''
},
tableData: [],
pageStore:{
currentPage: 1,
pageSize: 20,
total: 0
},
updatePersonStore:{
isShow: false,
row: {},
personnelName: '',
personnelCode: '',
phone: '',
status: '',
city: '',
cityArr: [],
county: '',
countyArr: []
},
personnelRules: {
name: [
personnelName: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
workId: [
personnelCode: [
{ required: true, message: '请输入工号', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择所属区域', trigger: 'change' }
]
status: [
{ required: true, message: '请选择状态', trigger: 'blur' }
],
county: [
{ required: true, message: '请选择所属区域', trigger: 'blur' }
],
},
regionOptions
isAddDialogOpen: false,
editingPersonnel: null,
currentPage: 1,
pageSize: 20,
personnelForm: {
name: '',
workId: '',
phone: '',
region: [],
status: 'enabled'
}
}
},
computed: {
...mapGetters(['user']),
hasProvincePermission() {
return this.user?.role === 'province_admin'
},
filteredPersonnel() {
return this.personnel.filter(person => {
const matchesQuery = !this.searchQuery ||
person.workId.includes(this.searchQuery) ||
person.phone.includes(this.searchQuery)
created(){
let pa = JSON.parse(localStorage.getItem('accountInfo'))
this.getData.city = pa.cityCode||''
this.getData.county = pa.countyCode||''
this.queryArea()
this.handleFilter()
},
methods: {
updatePerson(row){
let ud = this.updatePersonStore
if(row.id){
ud.row =row
ud.personnelName = row.personnelName
ud.personnelCode = row.personnelCode
ud.phone = row.phone
ud.status = row.status
ud.relatedMarketingCode = row.relatedMarketingCode
// 区域筛选逻辑
let matchesRegion = true
if (this.hasProvincePermission && this.selectedRegion && this.selectedRegion.length > 0) {
const selectedPath = this.selectedRegion.join('-')
matchesRegion = person.region.startsWith(selectedPath)
} else if (!this.hasProvincePermission && this.selectedRegionDistrict) {
matchesRegion = person.region.includes(this.selectedRegionDistrict)
}else{
ud.row = {}
ud.personnelName = ''
ud.personnelCode = ''
ud.phone = ''
ud.status = ''
ud.relatedMarketingCode = ''
if(!this.getData.city){
ud.city = ''
ud.county = ''
ud.countyArr = []
ud.grid = ''
ud.gridArr = []
}else if(!this.getData.county){
ud.county = ''
ud.grid = ''
ud.gridArr = []
}
}
this.updatePersonStore.isShow = true
},
cityChange(value,type){
let ad = type=='person'?this.updatePersonStore:this.addressStore
ad.city = value
ad.county = ''
ad.grid = ''
this.apiReq.queryLevelAllArea({
areaLevel: 3,
parentAreaCode: value
}).then(res=>{
if(res.code == 200){
ad.countyArr = res.data
if(type == 'all'){
this.updatePersonStore.countyArr = res.data
}
}
return matchesQuery && matchesRegion
})
},
paginatedPersonnel() {
const startIndex = (this.currentPage - 1) * this.pageSize
const endIndex = startIndex + this.pageSize
return this.filteredPersonnel.slice(startIndex, endIndex)
}
},
methods: {
timeRender(row,column){
return this.common.detailTime(new Date(row[column.property]).getTime())
},
handleFilter(){
this.pageStore.currentPage = 1
this.queryPersonList()
},
queryArea(){
this.apiReq.queryLevelAllArea({
areaLevel: 2,
parentAreaCode: '320000'
}).then(res=>{
if(res.code == 200){
this.updatePersonStore.cityArr = res.data
this.addressStore.cityArr = res.data
this.setAddressShow()
}
})
},
setAddressShow(){
let ad = this.addressStore
let pd = this.updatePersonStore
let gd = this.getData
if(gd.city){
ad.city = gd.city
this.cityChange(ad.city,'all')
if(gd.county){
ad.county = gd.county
}
}
pd.city = ad.city
pd.county = ad.county
},
handleSizeChange(size) {
this.pageStore.pageSize = size
this.handleFilter()
},
handleCurrentChange(page) {
this.pageStore.currentPage = page
this.queryPersonList()
},
queryPersonList(){
this.apiReq.queryAllPerson({
pageSize: this.pageStore.pageSize,
pageNum: this.pageStore.currentPage,
areaCode: this.addressStore.county || this.addressStore.city,
...this.formData
}).then(res=>{
if(res.code == 200){
this.tableData = res.data.records
this.pageStore.total = res.data.total
}else{
this.$message.error(res.message)
}
})
},
editPersonnel(person) {
this.editingPersonnel = person
// 分解区域路径为数组
......@@ -296,77 +421,50 @@ export default {
this.isAddDialogOpen = true
},
deletePersonnel(id) {
this.$confirm('确定要删除该营销人员吗?删除后该账号将无法登录。', '确认删除', {
this.$confirm('确定要删除该人员吗?此操作不可撤销。', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.personnel = this.personnel.filter(p => p.id !== id)
this.$message.success('政企业务营销人员删除成功')
}).catch(() => {
// 用户取消删除
this.apiReq.deletePerson({
id,
}).then(res=>{
if(res.code == '200'){
this.handleFilter()
this.$message.success('删除成功')
}else{
this.$message.error(res.message)
}
})
})
},
submitPersonnelForm() {
this.$refs.personnelForm.validate((valid) => {
if (valid) {
if (!this.personnelForm.name || !this.personnelForm.workId || !this.personnelForm.phone) {
this.$message.error('请填写所有必填项')
return
}
if (!this.personnelForm.region || this.personnelForm.region.length === 0) {
this.$message.error('请选择所属区域')
return
}
const region = this.personnelForm.region.join('-')
if (this.editingPersonnel) {
// 更新人员信息
const index = this.personnel.findIndex(p => p.id === this.editingPersonnel.id)
if (index !== -1) {
this.$set(this.personnel, index, {
...this.editingPersonnel,
...this.personnelForm,
region
})
this.$message.success('政企业务营销人员信息更新成功')
let ud = this.updatePersonStore
this.apiReq.addNewPerson({
personnelName: ud.personnelName,
personnelCode: ud.personnelCode,
phone: ud.phone,
personnelType: '3',
status: ud.status,
areaCode: ud.county
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('添加成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
__this.updatePersonStore.isShow = false
}
});
}else{
this.$message.error(res.message)
}
} else {
// 添加新人员
const newPersonnel = {
...this.personnelForm,
id: `EP${Date.now()}`,
region,
createTime: new Date().toLocaleString('zh-CN')
}
this.personnel.push(newPersonnel)
this.$message.success('政企业务营销人员添加成功')
}
// 重置表单和状态
this.resetPersonnelForm()
this.isAddDialogOpen = false
this.editingPersonnel = null
})
}
})
},
resetPersonnelForm() {
this.personnelForm = {
name: '',
workId: '',
phone: '',
region: [],
status: 'enabled'
}
},
handleSizeChange(size) {
this.pageSize = size
this.currentPage = 1
},
handleCurrentChange(page) {
this.currentPage = page
}
}
}
......
......@@ -5,78 +5,84 @@
<div class="toolbar-left">
<div class="search-wrapper">
<el-input
v-model="searchTerm"
v-model="formData.personnelCode"
placeholder="请输入工号查询..."
class="search-input"
clearable
/>
</div>
<div class="search-wrapper">
<el-input
v-model="searchPhone"
v-model="formData.phone"
placeholder="请输入手机号查询..."
class="search-input"
clearable
/>
</div>
<el-select v-model="filterType" placeholder="人员类型" class="filter-select">
<el-option label="全部类型" value="all"></el-option>
<el-option label="装维师傅" value="installer"></el-option>
<el-option label="营销人员" value="sales"></el-option>
<el-select v-model="formData.personnelTypes" placeholder="人员类型" class="filter-select" multiple clearable>
<el-option label="全部类型" value=""></el-option>
<el-option label="装维师傅" value="1"></el-option>
<el-option label="支撑人员" value="2"></el-option>
</el-select>
<template v-if="addressStore.cityArr.length>0">
<el-select
v-model="addressStore.city"
class="filter-select"
placeholder="选择地市"
@change="cityChange"
:disabled="getData.city!=''"
clearable>
<el-option
v-for="item in addressStore.cityArr"
:key="item.value"
:label="item.name"
:value="item.value"
></el-option>
</el-select>
</template>
<template v-if="addressStore.countyArr.length>0">
<el-select
v-model="addressStore.county"
class="filter-select"
placeholder="选择区县"
@change="countyChange"
:disabled="getData.county!=''"
clearable>
<el-option
v-for="item in addressStore.countyArr"
:key="item.value"
:label="item.name"
:value="item.value"
></el-option>
</el-select>
</template>
<template v-if="addressStore.gridArr.length>0">
<el-select
v-model="addressStore.grid"
class="filter-select"
placeholder="选择网格"
:disabled="getData.grid!=''"
clearable>
<el-option
v-for="item in addressStore.gridArr"
:key="item.value"
:label="item.name"
:value="item.value"
></el-option>
</el-select>
</template>
<el-select
v-model="addressStore.city"
class="filter-select"
placeholder="选择地市"
@change="cityChange"
:disabled="getData.city!=''"
clearable>
<el-option
v-for="item in addressStore.cityArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="addressStore.countyArr.length>0">
<el-select
v-model="addressStore.county"
class="filter-select"
placeholder="选择区县"
@change="countyChange"
:disabled="getData.county!=''"
clearable>
<el-option
v-for="item in addressStore.countyArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="addressStore.gridArr.length>0">
<el-select
v-model="addressStore.grid"
class="filter-select"
placeholder="选择网格"
:disabled="getData.grid!=''"
clearable>
<el-option
v-for="item in addressStore.gridArr"
:key="item.gridCode"
:label="item.gridName"
:value="item.gridCode"
></el-option>
</el-select>
</template>
</div>
<div class="toolbar-right">
<el-button @click="isImportDialogOpen = true" type="default" size="small">
<el-button size="small" @click="handleFilter">
<i class="el-icon-search"></i>
查询
</el-button>
<el-button @click="isImportDialogOpen = true" type="default" size="small" disabled>
<i class="el-icon-download"></i>
批量导入
</el-button>
<el-button @click="isAddDialogOpen = true" type="primary" size="small">
<el-button @click="updatePerson" type="primary" size="small">
<i class="el-icon-plus"></i>
添加人员
</el-button>
......@@ -85,44 +91,29 @@
<!-- 人员列表 -->
<div class="personnel-table">
<el-table :data="paginatedPersonnel" style="width: 100%" border>
<el-table-column prop="name" label="姓名" width="100"></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 label="人员类型" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.type === 'installer' ? 'info' : 'primary'">
{{ personnelTypeMap[scope.row.type].label }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="region" label="所属区域" width="150"></el-table-column>
<el-table-column prop="gridName" label="网格" width="100"></el-table-column>
<el-table-column label="关联营销人员" width="120">
<template slot-scope="scope">
<span v-if="scope.row.type === 'installer'">
<span v-if="getAssociatedSalesPerson(scope.row.associatedSales)">
{{ getAssociatedSalesPerson(scope.row.associatedSales).workId }}
</span>
<span v-else class="text-muted">未关联</span>
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table :data="tableData" style="width: 100%" border>
<el-table-column prop="personnelName" label="姓名" width="120"></el-table-column>
<el-table-column prop="personnelCode" label="工号" width="120"></el-table-column>
<el-table-column prop="phone" label="手机号" width="150"></el-table-column>
<el-table-column prop="personnelTypeName" label="人员类型" width="120"></el-table-column>
<el-table-column prop="areaName" label="所属区域" width="150"></el-table-column>
<el-table-column prop="gridName" label="网格" width="220"></el-table-column>
<el-table-column prop="relatedMarketingCode" label="关联支撑人员" width="120" ></el-table-column>
<el-table-column label="状态" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
<el-tag :type="scope.row.status == '1' ? 'success' : 'info'">
{{ scope.row.status == '1' ? '启用' : '关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="150"></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="200" :formatter="timeRender"></el-table-column>
<el-table-column label="操作" min-width="150">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="editPersonnel(scope.row)"
size="small"
disabled
@click="updatePerson(scope.row)"
>
编辑
</el-button>
......@@ -143,33 +134,34 @@
<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="filteredPersonnel.length"
:total="pageStore.total"
>
</el-pagination>
</div>
<!-- 添加/编辑人员对话框 -->
<el-dialog
:title="editingPersonnel ? '编辑人员' : '添加人员'"
:visible.sync="isAddDialogOpen"
:title=" updatePersonStore.row.id? '编辑人员' : '添加人员'"
:visible.sync="updatePersonStore.isShow"
class="addPersonDialog"
width="800px">
<el-form :model="personnelForm" :rules="personnelRules" ref="personnelForm" label-width="100px">
<el-form :model="updatePersonStore" :rules="personnelRules" ref="personnelForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="personnelForm.name" placeholder="请输入姓名"></el-input>
<el-form-item label="姓名" prop="personnelName">
<el-input v-model="updatePersonStore.personnelName" placeholder="请输入姓名"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工号" prop="workId">
<el-form-item label="工号" prop="personnelCode">
<el-input
v-model="personnelForm.workId"
v-model="updatePersonStore.personnelCode"
placeholder="请输入工号"
:disabled="!!editingPersonnel"
:disabled="!!updatePersonStore.row.id"
></el-input>
</el-form-item>
</el-col>
......@@ -178,38 +170,38 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input v-model="personnelForm.phone" placeholder="请输入手机号"></el-input>
<el-input v-model="updatePersonStore.phone" placeholder="请输入手机号"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="人员类型" prop="type">
<el-select v-model="personnelForm.type" @change="handlePersonnelTypeChange" style="width: 100%;">
<el-option label="装维师傅" value="installer"></el-option>
<el-option label="营销人员" value="sales"></el-option>
<el-form-item label="人员类型" prop="personnelType">
<el-select v-model="updatePersonStore.personnelType" @change="personnelTypeChange" style="width: 100%;">
<el-option label="装维师傅" value="1"></el-option>
<el-option label="支撑人员" value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<!-- 当选择装维师傅时显示关联营销人员字段 -->
<!-- 当选择装维师傅时显示关联支撑人员字段 -->
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="personnelForm.status" style="width: 100%;">
<el-option label="启用" value="active"></el-option>
<el-option label="禁用" value="inactive"></el-option>
<el-select v-model="updatePersonStore.status" style="width: 100%;">
<el-option label="启用" value="1"></el-option>
<el-option label="关闭" value="0"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="personnelForm.type === 'installer'">
<el-form-item label="关联营销人员" prop="associatedSales">
<el-select v-model="personnelForm.associatedSales" placeholder="请选择营销人员" style="width: 100%;">
<el-col :span="12" v-if="updatePersonStore.personnelType == '1'">
<el-form-item label="关联支撑人" prop="relatedMarketingCode">
<el-select v-model="updatePersonStore.relatedMarketingCode" placeholder="请选择支撑人员" style="width: 100%;">
<el-option
v-for="salesPerson in activeSalesPersons"
:key="salesPerson.id"
:label="`${salesPerson.name} (${salesPerson.workId})`"
:value="salesPerson.id"
v-for="item in yxPersonList"
:key="item.personnelCode"
:label="item.personnelName"
:value="item.personnelCode"
></el-option>
</el-select>
</el-form-item>
......@@ -218,50 +210,50 @@
<el-row :gutter="20">
<el-col :span="30">
<el-form-item label="所属区域" prop="region">
<template v-if="personnelForm.cityArr.length>0">
<el-form-item label="所属区域" prop="grid">
<template v-if="updatePersonStore.cityArr.length>0">
<el-select
v-model="personnelForm.city"
v-model="updatePersonStore.city"
placeholder="选择地市"
@change="(value)=>{cityChange(value,'person')}"
:disabled="getData.city!=''"
style="margin-right: 10px"
clearable>
<el-option
v-for="item in personnelForm.cityArr"
:key="item.value"
:label="item.name"
:value="item.value"
v-for="item in updatePersonStore.cityArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="personnelForm.countyArr.length>0">
<template v-if="updatePersonStore.countyArr.length>0">
<el-select
v-model="personnelForm.county"
v-model="updatePersonStore.county"
placeholder="选择区县"
@change="(value)=>{countyChange(value,'person')}"
:disabled="getData.county!=''"
style="margin-right: 10px"
clearable>
<el-option
v-for="item in personnelForm.countyArr"
:key="item.value"
:label="item.name"
:value="item.value"
v-for="item in updatePersonStore.countyArr"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</template>
<template v-if="personnelForm.gridArr.length>0">
<template v-if="updatePersonStore.gridArr.length>0">
<el-select
v-model="personnelForm.grid"
v-model="updatePersonStore.grid"
placeholder="选择网格"
:disabled="getData.grid!=''"
clearable>
<el-option
v-for="item in personnelForm.gridArr"
:key="item.value"
:label="item.name"
:value="item.value"
v-for="item in updatePersonStore.gridArr"
:key="item.gridCode"
:label="item.gridName"
:value="item.gridCode"
></el-option>
</el-select>
</template>
......@@ -271,7 +263,7 @@
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isAddDialogOpen = false">取消</el-button>
<el-button @click="updatePersonStore.isShow = false">取消</el-button>
<el-button type="primary" @click="submitPersonnelForm">确定</el-button>
</div>
</el-dialog>
......@@ -280,8 +272,7 @@
<el-dialog
title="批量导入人员"
:visible.sync="isImportDialogOpen"
width="600px"
>
width="600px">
<div class="import-content">
<div class="import-section">
<h4>选择导入文件</h4>
......@@ -297,15 +288,19 @@
<i class="el-icon-upload2"></i>
选择文件
</el-button>
<el-button @click="downloadTemplate('installer')" type="default">
<el-button @click="downloadTemplate('gongwei')" type="default">
<i class="el-icon-download"></i>
装维师傅模板
</el-button>
<el-button @click="downloadTemplate('sales')" type="default">
<el-button @click="downloadTemplate('yingxiao')" type="default">
<i class="el-icon-download"></i>
营销人员模板
支撑人员模板
</el-button>
</div>
<div class="file-upload">
<el-radio v-model="importStore.type" label="1">装修师傅</el-radio>
<el-radio v-model="importStore.type" label="2">支撑人员</el-radio>
</div>
<div class="file-info" v-if="selectedFile">
<span class="file-name">{{ selectedFile.name }}</span>
......@@ -330,160 +325,195 @@
</template>
<script>
// 人员类型映射
const personnelTypeMap = {
installer: { label: '装维师傅' },
sales: { label: '营销人员' }
}
// 模拟数据
const mockPersonnel = [
{ id: 'INS001', name: '王师傅', phone: '13987654321', workId: 'W001', type: 'installer', region: '南京市玄武区', gridName: 'A网格', status: 'active', createTime: '2025-09-01 09:00:00', associatedSales: 'SALE001' },
{ id: 'INS002', name: '李师傅', phone: '13876543210', workId: 'W002', type: 'installer', region: '南京市玄武区', gridName: 'B网格', status: 'active', createTime: '2025-09-01 09:00:00', associatedSales: 'SALE002' },
{ id: 'INS003', name: '张师傅', phone: '13765432109', workId: 'W003', type: 'installer', region: '南京市秦淮区', gridName: 'C网格', status: 'active', createTime: '2025-09-02 10:00:00', associatedSales: 'SALE003' },
{ id: 'INS004', name: '刘师傅', phone: '13654321098', workId: 'W004', type: 'installer', region: '南京市建邺区', gridName: 'D网格', status: 'active', createTime: '2025-09-03 11:00:00', associatedSales: 'SALE004' },
{ id: 'INS005', name: '陈师傅', phone: '13543210987', workId: 'W005', type: 'installer', region: '南京市鼓楼区', gridName: 'E网格', status: 'inactive', createTime: '2025-09-04 12:00:00', associatedSales: 'SALE005' },
{ id: 'INS006', name: '赵师傅', phone: '13432109876', workId: 'W006', type: 'installer', region: '南京市浦口区', gridName: 'F网格', status: 'active', createTime: '2025-09-05 13:00:00', associatedSales: 'SALE006' },
{ id: 'INS007', name: '孙师傅', phone: '13321098765', workId: 'W007', type: 'installer', region: '南京市栖霞区', gridName: 'G网格', status: 'active', createTime: '2025-09-06 14:00:00', associatedSales: 'SALE007' },
{ id: 'INS008', name: '周师傅', phone: '13210987654', workId: 'W008', type: 'installer', region: '南京市雨花台区', gridName: 'H网格', status: 'active', createTime: '2025-09-07 15:00:00', associatedSales: 'SALE008' },
{ id: 'INS009', name: '吴师傅', phone: '13109876543', workId: 'W009', type: 'installer', region: '南京市江宁区', gridName: 'I网格', status: 'active', createTime: '2025-09-08 16:00:00', associatedSales: 'SALE009' },
{ id: 'INS010', name: '郑师傅', phone: '13098765432', workId: 'W010', type: 'installer', region: '南京市六合区', gridName: 'J网格', status: 'inactive', createTime: '2025-09-09 17:00:00', associatedSales: 'SALE010' },
{ id: 'SALE001', name: '张营销', phone: '13912345678', workId: 'S001', type: 'sales', region: '南京市玄武区', gridName: 'A网格', status: 'active', createTime: '2025-09-01 09:00:00' },
{ id: 'SALE002', name: '陈营销', phone: '13923456789', workId: 'S002', type: 'sales', region: '南京市玄武区', gridName: 'B网格', status: 'active', createTime: '2025-09-01 09:00:00' },
{ id: 'SALE003', name: '杨营销', phone: '13934567890', workId: 'S003', type: 'sales', region: '南京市秦淮区', gridName: 'C网格', status: 'active', createTime: '2025-09-02 10:00:00' },
{ id: 'SALE004', name: '黄营销', phone: '13945678901', workId: 'S004', type: 'sales', region: '南京市建邺区', gridName: 'D网格', status: 'active', createTime: '2025-09-03 11:00:00' },
{ id: 'SALE005', name: '徐营销', phone: '13956789012', workId: 'S005', type: 'sales', region: '南京市鼓楼区', gridName: 'E网格', status: 'active', createTime: '2025-09-04 12:00:00' },
{ id: 'SALE006', name: '朱营销', phone: '13967890123', workId: 'S006', type: 'sales', region: '南京市浦口区', gridName: 'F网格', status: 'active', createTime: '2025-09-05 13:00:00' },
{ id: 'SALE007', name: '林营销', phone: '13978901234', workId: 'S007', type: 'sales', region: '南京市栖霞区', gridName: 'G网格', status: 'inactive', createTime: '2025-09-06 14:00:00' },
{ id: 'SALE008', name: '何营销', phone: '13989012345', workId: 'S008', type: 'sales', region: '南京市雨花台区', gridName: 'H网格', status: 'active', createTime: '2025-09-07 15:00:00' },
{ id: 'SALE009', name: '罗营销', phone: '13990123456', workId: 'S009', type: 'sales', region: '南京市江宁区', gridName: 'I网格', status: 'active', createTime: '2025-09-08 16:00:00' },
{ id: 'SALE010', name: '郭营销', phone: '13901234567', workId: 'S010', type: 'sales', region: '南京市六合区', gridName: 'J网格', status: 'active', createTime: '2025-09-09 17:00:00' },
{ id: 'INS011', name: '冯师傅', phone: '13812345678', workId: 'W011', type: 'installer', region: '南京市溧水区', gridName: 'K网格', status: 'active', createTime: '2025-09-10 18:00:00' },
{ id: 'INS012', name: '钱师傅', phone: '13823456789', workId: 'W012', type: 'installer', region: '南京市高淳区', gridName: 'L网格', status: 'active', createTime: '2025-09-11 19:00:00' },
{ id: 'SALE011', name: '宋营销', phone: '13834567890', workId: 'S011', type: 'sales', region: '南京市溧水区', gridName: 'K网格', status: 'active', createTime: '2025-09-10 18:00:00' },
{ id: 'SALE012', name: '梁营销', phone: '13845678901', workId: 'S012', type: 'sales', region: '南京市高淳区', gridName: 'L网格', status: 'inactive', createTime: '2025-09-11 19:00:00' }
]
export default {
name: 'PersonnelManagement',
data() {
return {
addressStore:{
importStore:{
isShow: false,
type: '1'
},
updatePersonStore:{
isShow: false,
row: {},
personnelName: '',
personnelCode: '',
relatedMarketingCode: '',
phone: '',
status: '',
city: '',
cityName: '',
cityArr: this.addressStoreData,
cityArr: [],
county: '',
countyName: '',
countyArr: [],
grid: '',
gridName: '',
gridArr: []
gridArr: [],
},
getData:{
city: '320200',
cityName: '',
yxPersonList:[],
formData:{
personnelCode: '',
personnelTypes: '',
personnelTypes: []
},
tableData: [],
pageStore:{
currentPage: 1,
pageSize: 20,
total: 0
},
addressStore:{
city: '',
cityArr: [],
county: '',
countyName: '',
countyArr: [],
grid: '',
gridName: '',
gridArr: []
},
personnelTypeMap,
personnel: mockPersonnel,
searchTerm: '',
searchPhone: '',
filterType: 'all',
filterRegion: '',
isAddDialogOpen: false,
isImportDialogOpen: false,
editingPersonnel: null,
selectedFile: null,
currentPage: 1,
pageSize: 20,
personnelForm: {
name: '',
workId: '',
phone: '',
type: 'installer',
getData:{
city: '',
cityArr: this.addressStoreData,
county: '',
countyArr: [],
grid: '',
gridArr: [],
status: 'active',
associatedSales: ''
},
personnelRules: {
name: [
personnelName: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
workId: [
personnelCode: [
{ required: true, message: '请输入工号', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' }
],
region: [
{ required: true, message: '请输入所属区域', trigger: 'blur' }
]
}
}
},
computed: {
filteredPersonnel() {
return this.personnel.filter(person => {
const matchesSearch = !this.searchTerm ||
person.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
person.workId.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
person.phone.includes(this.searchTerm)
const matchesType = !this.filterType || this.filterType === 'all' || person.type === this.filterType
// 区域筛选逻辑 - 只按区县筛选
const matchesRegion = !this.filterRegion || person.region.includes(this.filterRegion)
return matchesSearch && matchesType && matchesRegion
})
},
paginatedPersonnel() {
const startIndex = (this.currentPage - 1) * this.pageSize
const endIndex = startIndex + this.pageSize
return this.filteredPersonnel.slice(startIndex, endIndex)
},
activeSalesPersons() {
return this.personnel.filter(person => person.type === 'sales' && person.status === 'active')
personnelType: [
{ required: true, message: '请选择人员类型', trigger: 'blur' }
],
relatedMarketingCode: [
{ required: true, message: '请选择关联支撑人员', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'blur' }
],
grid: [
{ required: true, message: '请选择所属网格', trigger: 'blur' }
],
},
isImportDialogOpen: false,
selectedFile: null,
}
},
created(){
this.setAddressShow()
let pa = JSON.parse(localStorage.getItem('accountInfo'))
this.getData.city = pa.cityCode||''
this.getData.county = pa.countyCode||''
this.getData.grid = pa.gridCode||''
this.queryArea()
this.handleFilter()
this.queryYxPerson()
},
methods: {
queryArea(){
this.apiReq.queryLevelAllArea({
areaLevel: 2,
parentAreaCode: '320000'
}).then(res=>{
if(res.code == 200){
this.updatePersonStore.cityArr = res.data
this.addressStore.cityArr = res.data
this.setAddressShow()
}
})
},
personnelTypeChange(){
this.updatePersonStore.relatedMarketingCode = ''
},
queryYxPerson(){
this.apiReq.queryAllPerson({
pageSize: 20,
pageNum: 1,
personnelTypes: [2]
}).then(res=>{
if(res.code == 200){
this.yxPersonList = res.data.records
}
})
},
updatePerson(row){
let ud = this.updatePersonStore
if(row.id){
ud.row =row
ud.personnelName = row.personnelName
ud.personnelCode = row.personnelCode
ud.phone = row.phone
ud.status = row.status
ud.relatedMarketingCode = row.relatedMarketingCode
}else{
ud.row = {}
ud.personnelName = ''
ud.personnelCode = ''
ud.phone = ''
ud.status = ''
ud.relatedMarketingCode = ''
if(!this.getData.city){
ud.city = ''
ud.county = ''
ud.countyArr = []
ud.grid = ''
ud.gridArr = []
}else if(!this.getData.county){
ud.county = ''
ud.grid = ''
ud.gridArr = []
}else if(!this.getData.grid){
ud.grid = ''
}
}
this.updatePersonStore.isShow = true
},
timeRender(row,column){
return this.common.detailTime(new Date(row[column.property]).getTime())
},
handleFilter(){
this.pageStore.currentPage = 1
this.queryPersonList()
},
queryPersonList(){
this.apiReq.queryAllPerson({
pageSize: this.pageStore.pageSize,
pageNum: this.pageStore.currentPage,
areaCode: this.addressStore.county || this.addressStore.city,
gridCode: this.addressStore.grid,
...this.formData
}).then(res=>{
if(res.code == 200){
this.tableData = res.data.records
this.pageStore.total = res.data.total
}else{
this.$message.error(res.message)
}
})
},
setAddressShow(){
let ad = this.addressStore
let pd = this.personnelForm
let pd = this.updatePersonStore
let gd = this.getData
if(gd.city){
ad.city = gd.city
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
}
})
this.cityChange(ad.city,'all')
if(gd.county){
ad.county = gd.county
ad.countyArr.forEach(item=>{
if(item.value == ad.county){
ad.gridArr = item.children
}
})
if(ad.gridArr.length == 1){
gd.grid = 'moren'
}
this.countyChange(ad.county,'all')
if(gd.grid){
ad.grid = gd.grid
......@@ -492,101 +522,93 @@ export default {
}
pd.city = ad.city
pd.countyArr = ad.countyArr
pd.county = ad.county
pd.gridArr = ad.gridArr
pd.grid = ad.grid
},
cityChange(value,type){
let ad = type=='person'?this.personnelForm:this.addressStore
let ad = type=='person'?this.updatePersonStore:this.addressStore
ad.city = value
ad.county = ''
ad.grid = ''
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
this.apiReq.queryLevelAllArea({
areaLevel: 3,
parentAreaCode: value
}).then(res=>{
if(res.code == 200){
ad.countyArr = res.data
if(type == 'all'){
this.updatePersonStore.countyArr = res.data
}
}
})
},
countyChange(value,type){
let ad = type=='person'?this.personnelForm:this.addressStore
let ad = type=='person'?this.updatePersonStore:this.addressStore
ad.county = value
ad.grid = ''
ad.countyArr.forEach(item=>{
if(item.value == ad.county){
ad.gridArr = item.children
this.apiReq.queryGridList({
areaCode:value
}).then(res=>{
if(res.code == 200){
ad.gridArr = res.data
if(type == 'all'){
this.updatePersonStore.gridArr = res.data
}
}
})
},
handlePersonnelTypeChange(value) {
// 当人员类型改变时,重置关联字段
if (value === 'sales') {
this.personnelForm.associatedSales = ''
} else {
this.personnelForm.gridName = ''
}
},
editPersonnel(person) {
this.editingPersonnel = person
this.personnelForm = { ...person }
this.isAddDialogOpen = true
},
deletePersonnel(id) {
this.$confirm('确定要删除该人员吗?此操作不可撤销。', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.personnel = this.personnel.filter(p => p.id !== id)
this.$message.success('人员删除成功')
}).catch(() => {
// 用户取消删除
this.apiReq.deletePerson({
id,
}).then(res=>{
if(res.code == '200'){
this.handleFilter()
this.$message.success(res.message)
}else{
this.$message.error(res.message)
}
})
})
},
submitPersonnelForm() {
this.$refs.personnelForm.validate((valid) => {
if (valid) {
if (this.editingPersonnel) {
// 更新人员信息
const index = this.personnel.findIndex(p => p.id === this.editingPersonnel.id)
if (index !== -1) {
this.$set(this.personnel, index, { ...this.editingPersonnel, ...this.personnelForm })
this.$message.success('人员信息更新成功')
let ud = this.updatePersonStore
this.apiReq.addNewPerson({
personnelName: ud.personnelName,
personnelCode: ud.personnelCode,
phone: ud.phone,
personnelType: ud.personnelType,
status: ud.status,
relatedMarketingCode: ud.relatedMarketingCode,
areaCode: ud.county,
gridCode: ud.grid
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('添加成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
__this.updatePersonStore.isShow = false
}
});
}else{
this.$message.error(res.message)
}
} else {
// 添加新人员
const newPersonnel = {
...this.personnelForm,
id: `NEW${Date.now()}`,
createTime: new Date().toLocaleString('zh-CN')
}
this.personnel.push(newPersonnel)
this.$message.success('人员添加成功')
}
// 重置表单和状态
this.resetPersonnelForm()
this.isAddDialogOpen = false
this.editingPersonnel = null
})
}
})
},
resetPersonnelForm() {
this.personnelForm = {
name: '',
workId: '',
phone: '',
type: 'installer',
region: '',
gridName: '',
status: 'active',
associatedSales: ''
}
},
handleFileSelect(event) {
const file = event.target.files[0]
if (file) {
......@@ -594,62 +616,7 @@ export default {
}
},
downloadTemplate(type) {
// 创建Excel模板数据
const headers = type === 'installer'
? ['工号', '手机号', '姓名', '网格名称', '对应营销人员工号', '对应营销人员手机号', '对应营销人员姓名']
: ['工号', '手机号', '姓名', '网格名称']
const sampleData = type === 'installer'
? [
['IF001', '13812345678', '张师傅', '玄武区A网格', 'SA001', '13898765432', '李营销'],
['IF002', '13887654321', '王师傅', '秦淮区B网格', 'SA002', '13876543210', '赵营销']
]
: [
['SA001', '13898765432', '李营销', '玄武区A网格'],
['SA002', '13876543210', '赵营销', '秦淮区B网格']
]
// 创建Excel格式内容(HTML表格格式,Excel能正确识别)
const excelContent = `
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta charset="utf-8">
<!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>Sheet1</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->
</head>
<body>
<table>
<tr>
${headers.map(header => `<th style="background-color: #f0f0f0; font-weight: bold; border: 1px solid #ccc; padding: 8px;">${header}</th>`).join('')}
</tr>
${sampleData.map(row =>
`<tr>
${row.map(cell => `<td style="border: 1px solid #ccc; padding: 8px;">${cell}</td>`).join('')}
</tr>`
).join('')}
</table>
</body>
</html>
`
// 创建Blob并下载
const blob = new Blob([excelContent], {
type: 'application/vnd.ms-excel;charset=utf-8;'
})
// 下载文件
const link = document.createElement('a')
if (link.download !== undefined) {
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `${type === 'installer' ? '装维师傅' : '营销人员'}导入模板.xls`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
this.$message.success(`${type === 'installer' ? '装维师傅' : '营销人员'}模板下载成功`)
this.common.downloadLocalFile('static/file/'+type+'_tem.xlsx', 'application/xlsx', (type=='gongwei'?'工维人员批量新增模板':'营销人员批量新增模板'), 'xlsx')
},
handleBatchImport() {
if (!this.selectedFile) {
......@@ -662,17 +629,16 @@ export default {
this.isImportDialogOpen = false
this.selectedFile = null
},
getAssociatedSalesPerson(associatedSalesId) {
if (!associatedSalesId) return null
return this.personnel.find(p => p.id === associatedSalesId)
},
handleSizeChange(size) {
this.pageSize = size
this.currentPage = 1
this.pageStore.pageSize = size
this.handleFilter()
},
handleCurrentChange(page) {
this.currentPage = page
}
this.pageStore.currentPage = page
this.queryPersonList()
},
}
}
</script>
......
......@@ -2,7 +2,7 @@
<div class="tag-management">
<div class="toolbar">
<p class="description">管理商机录入时的标签选项</p>
<el-button @click="isAddDialogOpen = true" type="primary" size="small">
<el-button @click="addTag" type="primary" size="small">
<i class="el-icon-plus"></i>
添加标签
</el-button>
......@@ -16,34 +16,34 @@
:md="8"
:lg="8"
:xl="6"
v-for="tag in tags"
v-for="tag in tagsList"
:key="tag.id"
>
<el-card class="tag-card">
<div class="tag-header">
<div class="tag-title" v-if="tag.isSystem">
<h3>{{ tag.name }}</h3>
<div class="tag-title" v-if="tag.isDefault" style="display: flex;align-items:center;">
<h3>{{ tag.tagName }}</h3>
<el-tag size="mini" type="info" style="margin-left: 8px;">系统默认</el-tag>
</div>
<h3 v-else>{{ tag.name }}</h3>
<h3 v-else>{{ tag.tagName }}</h3>
</div>
<div class="tag-info" v-if="tag.category">
<div class="tag-info" v-if="tag.tagCategoryName">
<i class="el-icon-folder"></i>
<span>分类:{{ tag.category }}</span>
<span>分类:{{ tag.tagCategoryName }}</span>
</div>
<div class="tag-description" v-if="tag.description">
<div class="tag-description">
<i class="el-icon-document"></i>
<span>{{ tag.description }}</span>
<span>{{ tag.tagDescription || '暂无' }}</span>
</div>
<div class="tag-footer">
<el-switch
v-model="tag.enabled"
v-model="tag.isEnabled"
active-text="启用"
inactive-text="禁用"
@change="toggleTag(tag.id)"
@change="toggleTag(tag.id,tag.isEnabled)"
:disabled="tag.isSystem"
></el-switch>
......@@ -51,14 +51,13 @@
<el-button
size="mini"
@click="editTag(tag)"
:disabled="tag.isSystem"
>
编辑
</el-button>
<el-button
size="mini"
@click="deleteTag(tag.id)"
v-if="!tag.isSystem"
v-if="!tag.isDefault"
>
删除
</el-button>
......@@ -69,42 +68,55 @@
</el-row>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageStore.currentPage"
:page-sizes="[20, 50, 100]"
:page-size="pageStore.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageStore.total"
>
</el-pagination>
</div>
<!-- 添加/编辑标签对话框 -->
<el-dialog
:title="editingTag ? '编辑标签' : '添加标签'"
:visible.sync="isAddDialogOpen"
width="500px"
>
width="500px">
<el-form :model="tagForm" :rules="tagRules" ref="tagForm" label-width="100px">
<el-form-item label="标签名称" prop="name">
<el-form-item label="标签名称" prop="tagName">
<el-input
v-model="tagForm.name"
v-model="tagForm.tagName"
placeholder="请输入标签名称"
:disabled="!!editingTag && editingTag.isSystem"
:disabled="!!editingTag && editingTag.isDefault"
></el-input>
<p v-if="editingTag && editingTag.isSystem" class="system-tag-info">系统默认标签只能修改描述信息</p>
<p v-if="editingTag && editingTag.isDefault" class="system-tag-info">系统默认标签只能修改描述信息</p>
</el-form-item>
<el-form-item label="标签分类" prop="category">
<el-form-item label="标签分类" prop="tagCategoryName">
<el-input
v-model="tagForm.category"
placeholder="请输����标签分类"
:disabled="!!editingTag && editingTag.isSystem"
v-model="tagForm.tagCategoryName"
placeholder="请输标签分类"
:disabled="!!editingTag && editingTag.isDefault"
></el-input>
</el-form-item>
<el-form-item label="标签描述" prop="description">
<el-input
v-model="tagForm.description"
v-model="tagForm.tagDescription"
type="textarea"
:rows="3"
placeholder="可选,描述此标签的用途"
></el-input>
</el-form-item>
<el-form-item label="启用标签" v-if="!(editingTag && editingTag.isSystem)">
<el-form-item label="启用标签" v-if="!(editingTag && editingTag.isDefault)">
<el-switch
v-model="tagForm.enabled"
v-model="tagForm.isEnabled"
></el-switch>
</el-form-item>
</el-form>
......@@ -118,39 +130,82 @@
</template>
<script>
// 模拟标签数据
const mockTags = [
{ id: 'SYS001', name: '小微工程', color: '#3b82f6', order: 1, description: '选择该标签后商机自动分配给区县负责人', enabled: true, category: '小微工程', isSystem: true },
{ id: 'SYS002', name: '政企业务', color: '#10b981', order: 2, description: '选择该标签后商机自动分配给区县负责人', enabled: true, category: '政企业务', isSystem: true },
{ id: '3', name: '网络升级', color: '#f59e0b', order: 3, description: '用户需要升级网络带宽', enabled: true, category: '产品服务' },
{ id: '4', name: '融合套餐', color: '#ef4444', order: 4, description: '手机+宽带融合套餐', enabled: true, category: '业务类型' },
{ id: '5', name: '智能家居', color: '#8b5cf6', order: 5, description: '智能家居产品需求', enabled: false, category: '产品服务' },
{ id: '6', name: '家庭安防', color: '#ec4899', order: 6, description: '家庭安防监控需求', enabled: true, category: '产品服务' }
]
export default {
name: 'TagManagement',
data() {
return {
tags: mockTags,
tagsList: [],
pageStore:{
currentPage: 1,
pageSize: 20,
total: 0
},
isAddDialogOpen: false,
editingTag: null,
tagForm: {
name: '',
color: '#3b82f6',
tagName: '',
order: 1,
description: '',
enabled: true,
category: ''
tagDescription: '',
isEnabled: true,
tagCategoryName: '',
isDefault: false
},
tagRules: {
name: [
tagName: [
{ required: true, message: '请输入标签名称', trigger: 'blur' }
],
tagCategoryName: [
{ required: true, message: '请输入标签分类', trigger: 'blur' }
]
}
},
}
},
created(){
this.handleFilter()
},
methods: {
handleSizeChange(size) {
this.pageStore.pageSize = size
this.handleFilter()
},
handleCurrentChange(page) {
this.pageStore.currentPage = page
this.queryBusiLabel()
},
handleFilter(){
this.pageStore.currentPage = 1
this.queryBusiLabel()
},
queryBusiLabel(){
this.apiReq.queryBusiLabelList({
pageNum: this.pageStore.currentPage,
pageSize: this.pageStore.pageSize
}).then(res=>{
if(res.code == 200){
this.tagsList = res.data.records
this.pageStore.total = res.data.total
}
})
},
addTag(){
this.editingTag = null
this.tagForm = {
tagName: '',
tagDescription: '',
isEnabled: true,
tagCategoryName: '',
tagCategory: '3',
isDefault: false
}
this.isAddDialogOpen = true
},
editTag(tag) {
this.editingTag = tag
this.tagForm = { ...tag }
......@@ -162,63 +217,63 @@ export default {
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.tags = this.tags.filter(t => t.id !== id)
this.$message.success('标签删除成功')
}).catch(() => {
// 用户取消删除
this.apiReq.deleteBusiLabel({
id,
}).then(res=>{
if(res.code == 200){
let __this = this
this.$alert('删除成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
}
});
}else{
this.$message.error(res.message)
}
})
})
},
toggleTag(id) {
const tag = this.tags.find(t => t.id === id)
if (tag) {
const action = tag.enabled ? '开启' : '关闭'
this.$message.success(`标签已${action}`)
}
toggleTag(id,enabled) {
this.apiReq.updateBusiLabelStatus({
id,
enabled
}).then(res=>{
if(res.code == 200){
}else{
this.$message.error(res.message)
}
})
},
submitTagForm() {
this.$refs.tagForm.validate((valid) => {
if (valid) {
if (this.editingTag) {
// 更新标签信息
const index = this.tags.findIndex(t => t.id === this.editingTag.id)
if (index !== -1) {
// 保留系统标签的特殊属性
const isSystem = this.tags[index].isSystem
this.$set(this.tags, index, {
...this.tags[index],
...this.tagForm,
isSystem
})
this.$message.success('标签更新成功')
this.apiReq.createAndUpdateTag({
id: this.editingTag?this.editingTag.id: '',
tagCategory: this.tagForm.tagCategory,
tagCategoryName: this.tagForm.tagCategoryName,
tagDescription: this.tagForm.tagDescription,
tagName: this.tagForm.tagName,
isDefault: this.tagForm.isDefault?1:0,
isEnabled: this.tagForm.isEnabled?1:0,
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert(this.editingTag?'修改成功':'添加成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
__this.isAddDialogOpen = false
}
});
}else{
this.$message.error(res.message)
}
} else {
// 添加新标签
const newTag = {
...this.tagForm,
id: `TAG${Date.now()}`,
order: this.tags.length + 1
}
this.tags.push(newTag)
this.$message.success('标签添加成功')
}
// 重置表单和状态
this.resetTagForm()
this.isAddDialogOpen = false
this.editingTag = null
})
}
})
},
resetTagForm() {
this.tagForm = {
name: '',
color: '#3b82f6',
order: 1,
description: '',
enabled: true,
category: ''
}
}
}
}
</script>
......@@ -260,6 +315,7 @@ export default {
display: flex;
align-items: center;
margin-bottom: 10px;
height: 18px;
font-size: 14px;
color: #606266;
......
......@@ -91,7 +91,7 @@ export default new Vuex.Store({
return new Promise((resolve, reject) => {
// 模拟登录验证
const userData = mockUsers[username]
if (userData && userData.password === password) {
if (userData && userData.password == password) {
commit('SET_USER', userData.user)
localStorage.setItem('user', JSON.stringify(userData.user))
resolve(true)
......
......@@ -35,10 +35,9 @@
<template v-for="item in menuItems">
<!-- 有子菜单的项 -->
<el-submenu
v-if="item.subItems && hasPermission(item.permissions)"
v-if="item.subItems"
:key="item.title"
:index="item.title"
>
:index="item.title" >
<template slot="title">
<i :class="item.icon"></i>
<span slot="title">{{ item.title }}</span>
......@@ -46,8 +45,8 @@
<el-menu-item
v-for="subItem in item.subItems"
:key="subItem.url"
:index="subItem.url"
>
v-if="hasPermissionChild(subItem.url)"
:index="subItem.url">
<i :class="subItem.icon"></i>
<span slot="title">{{ subItem.title }}</span>
</el-menu-item>
......@@ -55,7 +54,7 @@
<!-- 无子菜单的项 -->
<el-menu-item
v-else-if="item.url && hasPermission(item.permissions)"
v-else-if="item.url && hasPermission(item.url)"
:key="item.title"
:index="item.url"
>
......@@ -75,17 +74,17 @@
</el-avatar>
<transition name="fade">
<div v-if="!isCollapse" class="user-details">
<span class="user-name">{{ currentUser.name }}</span>
<span class="user-region">{{ currentUser.region }}</span>
<span class="user-name">{{ acc.accountName }}</span>
<span class="user-region">{{ acc.areaName }}</span>
</div>
</transition>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="profile">
<!-- <el-dropdown-item command="profile">
<i class="el-icon-user"></i>
个人信息
</el-dropdown-item>
<el-dropdown-item divided command="logout">
</el-dropdown-item> -->
<el-dropdown-item command="logout">
<i class="el-icon-switch-button"></i>
退出登录
</el-dropdown-item>
......@@ -120,7 +119,7 @@
<div class="header-right">
<span v-if="isDemoMode" class="demo-badge">演示模式</span>
<span class="current-region">当前区域: {{ currentUser.region }}</span>
<span class="current-region">当前区域: {{ acc.areaName }}</span>
</div>
</el-header>
......@@ -201,22 +200,19 @@ export default {
return {
isCollapse: false,
menuItems: menuItems,
breadcrumbMap: breadcrumbMap
breadcrumbMap: breadcrumbMap,
acc: JSON.parse(localStorage.getItem('accountInfo'))
}
},
computed: {
...mapGetters(['user', 'isAuthenticated']),
currentUser() {
return this.user || {}
},
userInitial() {
return this.currentUser.name ? this.currentUser.name.charAt(0) : ''
return this.acc.accountName ? this.acc.accountName.charAt(0) : ''
},
userAvatar() {
return ''
},
isDemoMode() {
return sessionStorage.getItem('demoMode') === 'true'
return sessionStorage.getItem('demoMode') == 'true'
},
activeMenu() {
const { meta, path } = this.$route
......@@ -245,18 +241,43 @@ export default {
toggleCollapse() {
this.isCollapse = !this.isCollapse
},
hasPermission(permissions) {
if (!this.user || !this.user.permissions) return false
return permissions.some(permission => this.user.permissions.includes(permission))
hasPermission(url) {
if(url=='/grid-query' && this.acc.gridCode){
return false
}
return true
},
async handleUserCommand(command) {
if (command === 'logout') {
await this.logout()
this.$message.success('退出登录成功')
this.$router.push('/login')
} else if (command === 'profile') {
this.$message.info('个人信息功能开发中')
hasPermissionChild(url){
if(url == '/system/opportunity-config'){
if(this.acc.cityCode && !this.acc.countyCode){
return true
}
return false
}
return true
},
async handleUserCommand(command) {
this.apiReq.logout().then(res=>{
if(res.code == '200'){
localStorage.removeItem('accountInfo')
localStorage.removeItem('tokenInfo')
this.$router.push('/login')
}else{
this.$message.error(res.message)
}
})
// if (command == 'logout') {
// await this.logout()
// this.$message.success('退出登录成功')
// this.$router.push('/login')
// } else if (command == 'profile') {
// this.$message.info('个人信息功能开发中')
// }
}
},
created() {
......
......@@ -5,28 +5,15 @@
</div>
<el-card class="filter-card">
<div class="filter-header">
<h3 class="filter-title">查询条件</h3>
<div class="filter-actions">
<el-button size="small" @click="handleQuery">
<i class="el-icon-search"></i>
查询
</el-button>
<el-button size="small" @click="resetQuery" type="default">
<i class="el-icon-refresh-left"></i>
重置
</el-button>
</div>
</div>
<div class="filter-content">
<el-row :gutter="24">
<el-col :span="3">
<div class="search-wrapper">
<el-input
v-model="selectedGridName"
v-model="formData.gridName"
placeholder="请输入网格名称"
style="padding-left: 0;"
clearable
@keyup.enter.native="handleFilter"
/>
</div>
......@@ -35,9 +22,10 @@
<el-col :span="3">
<div class="search-wrapper">
<el-input
v-model="selectedGrid"
v-model="formData.gridCode"
placeholder="请输入网格ID"
style="padding-left: 0;"
clearable
@keyup.enter.native="handleFilter"
/>
</div>
......@@ -48,13 +36,12 @@
v-model="addressStore.city"
placeholder="选择地市"
@change="cityChange"
:disabled="getData.city!=''"
clearable>
<el-option
v-for="item in addressStore.cityArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</el-col>
......@@ -63,16 +50,28 @@
v-model="addressStore.county"
placeholder="选择区县"
@change="countyChange"
:disabled="getData.county!=''"
clearable>
<el-option
v-for="item in addressStore.countyArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</el-col>
<el-col :span="3">
<div class="filter-actions">
<el-button size="small" @click="handleQuery">
<i class="el-icon-search"></i>
查询
</el-button>
<el-button size="small" @click="resetQuery" type="default">
<i class="el-icon-refresh-left"></i>
重置
</el-button>
</div>
</el-col>
</el-row>
</div>
</el-card>
......@@ -88,9 +87,9 @@
:data="tableData"
border
>
<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-column prop="gridName" label="网格名称" width="220"></el-table-column>
<el-table-column prop="gridCode" label="网格ID" width="220"></el-table-column>
<el-table-column prop="areaName" label="所属区域" min-width="250"></el-table-column>
</el-table>
<!-- 分页 -->
......@@ -118,100 +117,103 @@ export default {
return {
addressStore:{
city: '',
cityName: '',
cityArr: this.addressStoreData,
cityArr: [],
county: '',
countyName: '',
countyArr: [],
},
getData:{
city: '',
cityName: '',
county: '',
countyName: ''
formData:{
gridName: '',
gridCode: '',
},
selectedRegion: [],
selectedGrid: '',
selectedGridName: '',
tableData: [],
pageStore:{
pageStore: {
currentPage: 1,
pageSize: 20,
total: 0
}
},
tableData: []
}
},
created(){
this.queryArea()
this.handleQuery()
},
methods: {
setAddressShow(){
let ad = this.addressStore
let gd = this.getData
if(gd.city){
ad.city = gd.city
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
}
})
if(gd.county){
ad.county = gd.county
ad.countyArr.forEach(item=>{
if(item.value == ad.county){
ad.gridArr = item.children
}
})
if(ad.gridArr.length == 1){
gd.grid = 'moren'
}
if(gd.grid){
ad.grid = gd.grid
}
queryArea(){
this.apiReq.queryLevelAllArea({
areaLevel: 2,
parentAreaCode: '320000'
}).then(res=>{
if(res.code == 200){
this.addressStore.cityArr = res.data
}
}
})
},
cityChange(value){
let ad = this.addressStore
ad.city = value
ad.county = ''
ad.grid = ''
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
this.apiReq.queryLevelAllArea({
areaLevel: 3,
parentAreaCode: value
}).then(res=>{
if(res.code == 200){
ad.countyArr = res.data
}
})
},
handleQuery() {
this.currentPage = 1
this.$message.success('查询成功')
this.pageStore.currentPage = 1
this.queryGridData()
},
queryGridData(){
if(this.addressStore.city && !this.addressStore.county){
this.$message.error('请选择区县')
return
}
this.apiReq.queryAllGridList({
areaCode: this.addressStore.county,
pageSize: this.pageStore.pageSize,
pageNum: this.pageStore.currentPage,
...this.formData
}).then(res=>{
if(res.code == 200){
this.tableData = res.data.records
this.pageStore.total = res.data.total
}
})
},
resetQuery() {
this.selectedRegion = []
this.selectedGrid = ''
this.selectedGridName = ''
this.formData.gridCode = ''
this.formData.gridName = ''
this.addressStore.city = ''
this.addressStore.county = ''
this.addressStore.countyArr = []
},
handleSizeChange(size) {
this.pageSize = size
this.currentPage = 1
this.pageStore.pageSize = size
this.handleQuery()
},
handleCurrentChange(page) {
this.currentPage = page
}
this.pageStore.currentPage = page
this.queryGridData()
},
}
}
</script>
<style lang="scss" scoped>
.grid-query {
.el-button.el-button--small{
padding: 13px 12px;
}
.page-header {
margin-bottom: 24px;
......
......@@ -16,7 +16,7 @@
智能商机管理<br>助力业务增长
</h2>
<p class="login-desc">
装维师傅上门服务时发现商机,系统自动分配给营销人员跟进,实现业务闭环管理
装维师傅上门服务时发现商机,系统自动分配给支撑人员跟进,实现业务闭环管理
</p>
</div>
......@@ -38,7 +38,7 @@
</div>
<div class="login-feature-text">
<h3 class="login-feature-title">智能分配</h3>
<p class="login-feature-desc">基于网格自动分配给合适的营销人员</p>
<p class="login-feature-desc">基于网格自动分配给合适的支撑人员</p>
</div>
</div>
......@@ -75,23 +75,21 @@
<div class="login-tabs">
<div
class="login-tab"
:class="{ active: activeTab === 'account' }"
@click="activeTab = 'account'"
>
:class="{ active: activeTab == 'account' }"
@click="activeTab = 'account'">
账号密码登录
</div>
<div
class="login-tab"
:class="{ active: activeTab === 'phone' }"
@click="activeTab = 'phone'"
>
:class="{ active: activeTab == 'phone' }"
@click="activeTab = 'phone'">
手机验证码登录
</div>
</div>
<!-- 账号密码登录表单 -->
<el-form
v-if="activeTab === 'account'"
v-if="activeTab == 'account'"
:model="loginForm"
:rules="loginRules"
ref="loginForm"
......@@ -143,7 +141,7 @@
<!-- 手机验证码登录表单 -->
<el-form
v-if="activeTab === 'phone'"
v-if="activeTab == 'phone'"
:model="phoneForm"
:rules="phoneRules"
ref="phoneForm"
......@@ -224,10 +222,6 @@
<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>
......@@ -247,8 +241,8 @@ export default {
return {
activeTab: 'account', // 当前激活的登录方式
loginForm: {
username: '',
password: ''
username: 'ntadmin',
password: 'ntadmin'
},
phoneForm: {
phone: '',
......@@ -289,7 +283,7 @@ export default {
computed: {
// 是否可以发送验证码
canSendCode() {
return this.phoneForm.phone && this.phoneForm.captcha && this.phoneForm.captcha === this.captchaText
return this.phoneForm.phone && this.phoneForm.captcha
}
},
mounted() {
......@@ -310,7 +304,6 @@ export default {
try {
await this.$refs.loginForm.validate()
this.apiReq.login({
username: ld.username,
password: ld.password
......@@ -332,104 +325,59 @@ export default {
},
// 刷新图形验证码
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.apiReq.getImgCode().then(res=>{
if(res.code == 200){
this.captchaImage = 'data:image/png;base64,'+res.data.img
this.captchaId = res.data.codeId
}
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 = ''
this.apiReq.getTelCode({
phone: this.phoneForm.phone,
captchaCode: this.phoneForm.captcha,
captchaId: this.captchaId
}).then(res=>{
if(res.code == 200){
this.$message.success('验证码发送成功')
// 开始倒计时
this.codeCountdown = 60
this.countdownTimer = setInterval(() => {
this.codeCountdown--
if (this.codeCountdown <= 0) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
}, 1000)
}else{
this.$message.error(res.message)
}
}
})
},
// 手机验证码登录
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' }
}
this.apiReq.pohoneLogin({
phone: this.phoneForm.phone,
personnelCode: this.phoneForm.phone,
smsCode: this.phoneForm.phoneCode,
rememberMe: true
}).then(res=>{
if(res.code == 200){
localStorage.setItem('accountInfo',JSON.stringify(res.data.account))
localStorage.setItem('tokenInfo',JSON.stringify(res.data.tokenInfo))
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('手机号或验证码错误')
}
this.$router.push('/')
}else{
this.$message.error(res.message)
}
})
} catch (error) {
console.error('Phone login error:', error)
......@@ -659,7 +607,7 @@ export default {
.captcha-image {
flex-shrink: 0;
width: 100px;
height: 40px;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
......
......@@ -35,14 +35,14 @@
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<span class="info-label">客户地址</span>
<span class="info-value">{{ opportunity.customerAddress }}</span>
<span class="info-label">客户联系方式</span>
<span class="info-value">{{ opportunity.customerPhone}}</span>
</div>
</el-col>
<el-col :span="10">
<el-col :span="14">
<div class="info-item">
<span class="info-label">客户联系方式</span>
<span class="info-value">{{ opportunity.customerPhone }}</span>
<span class="info-label">客户地址</span>
<span class="info-value">{{ getAddressShow(opportunity.customerAddress) }}</span>
</div>
</el-col>
</el-row>
......@@ -58,19 +58,19 @@
<el-col :span="8">
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">{{ opportunity.installerName }} ({{ opportunity.installerId }})</span>
<span class="info-value">{{ opportunity.maintenanceStaffName || -'' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">联系电话:</span>
<span class="info-value">{{ opportunity.installerPhone }}</span>
<span class="info-value">{{ opportunity.maintenanceStaffPhone || -'' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">工号:</span>
<span class="info-value">{{ opportunity.assignedToName || '-' }} ({{ opportunity.assignedTo || '-' }})</span>
<span class="info-value">{{ opportunity.maintenanceStaffNo || '-' }}</span>
</div>
</el-col>
</el-row>
......@@ -79,26 +79,26 @@
<el-card class="detail-card">
<div class="card-header">
<h3>营销人员信息</h3>
<h3>支撑人员信息</h3>
</div>
<div class="card-content">
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">{{ opportunity.installerName }} ({{ opportunity.installerId }})</span>
<span class="info-value">{{ opportunity.marketingStaffName || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">电话:</span>
<span class="info-value">{{ opportunity.installerPhone }}</span>
<span class="info-value">{{ opportunity.marketingStaffPhone || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">工号:</span>
<span class="info-value">{{ opportunity.assignedToName || '-' }} ({{ opportunity.assignedTo || '-' }})</span>
<span class="info-value">{{ opportunity.marketingStaffNo || '-' }}</span>
</div>
</el-col>
</el-row>
......@@ -114,7 +114,7 @@
<span class="info-label">商机标签:</span>
<span class="info-value">
<el-tag
v-for="tag in opportunity.tags"
v-for="tag in opportunity.tagNames"
:key="tag"
size="small"
style="margin-right: 8px;"
......@@ -126,7 +126,19 @@
<div class="info-item" style="margin-top: 15px;">
<span class="info-label">商机描述:</span>
<span class="info-value">{{ opportunity.description }}</span>
<span class="info-value">{{ opportunity.textDescription || '--'}}</span>
</div>
<div class="info-item" style="margin-top: 15px;display: block" v-if="opportunity.voiceDescriptionUrl>0">
<div class="info-label">语音描述:</div>
<div class="info-value">
<audio class="single-audio" controls v-for="item in opportunity.audioArr">
<source :src="item" type="audio/wav">
<source :src="item" type="audio/mp3">
<source :src="item" type="audio/ogg">
您的浏览器不支持音频播放
</audio>
</div>
</div>
<div>
......@@ -142,14 +154,21 @@
<div class="card-content">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
v-for="(activity, index) in followList"
:key="index"
:timestamp="activity.timestamp"
:type="activity.type"
:timestamp="timeRender(activity.createTime)"
type="success"
>
<div class="activity-content">
<p>{{ activity.content }}</p>
<p v-if="activity.operator" class="operator">操作人:{{ activity.operator }}</p>
<p>{{ activity.followContent }}</p>
<p v-if="activity.followPersonName" class="operator">操作人:{{ activity.followPersonName }}</p>
<el-image
v-for="(item,index) in activity.imgArr"
:src="item"
:initial-index="index"
:preview-src-list="activity.imgArr"
style="width: 60px; height: 60px;margin-right: 10px;"
/>
</div>
</el-timeline-item>
</el-timeline>
......@@ -173,6 +192,7 @@
icon="el-icon-edit"
class="action-btn"
@click="assignBusi"
:disabled="opportunity.status!=1 && opportunity.status!=2"
>
分配商机
</el-button>
......@@ -180,7 +200,8 @@
type="success"
icon="el-icon-s-promotion"
class="action-btn"
@click="makeOrder"
@click="dealBusi"
:disabled="opportunity.status!=1 && opportunity.status!=2"
>
标记成单
</el-button>
......@@ -189,6 +210,7 @@
icon="el-icon-s-check"
class="action-btn"
@click="audioBusi"
:disabled="opportunity.status!=3"
>
商机审核
</el-button>
......@@ -197,6 +219,7 @@
icon="el-icon-circle-close"
class="action-btn"
@click="shutBusi"
:disabled="opportunity.status==5 || opportunity.status==4"
>
关闭商机
</el-button>
......@@ -213,19 +236,19 @@
<div class="key-info">
<div class="key-item">
<span class="key-label">创建时间:</span>
<span class="key-value">{{ opportunity.region }}</span>
<span class="key-value">{{ timeRender(opportunity.createTime) }}</span>
</div>
<div class="key-item">
<span class="key-label">分配时间:</span>
<span class="key-value">{{ opportunity.region }}</span>
<span class="key-label">所属区域</span>
<span class="key-value">{{ opportunity.areaName }}</span>
</div>
<div class="key-item">
<span class="key-label">处理人:</span>
<span class="key-value">{{ opportunity.gridName }}</span>
<span class="key-value">{{ opportunity.processorName }}</span>
</div>
<div class="key-item">
<span class="key-label">最新跟进时间:</span>
<span class="key-value">{{ opportunity.gridName }}</span>
<span class="key-value">{{ timeRender(opportunity.latestFollowTime)}}</span>
</div>
</div>
</div>
......@@ -239,6 +262,7 @@
type="text"
icon="el-icon-edit-outline"
class="action-btn"
:disabled="opportunity.status==5 || opportunity.status==4"
@click="memoStore.isEdit = true"
>
编辑
......@@ -261,12 +285,58 @@
</div>
<div v-if="memoStore.isEdit" style="margin-top: 10px;">
<el-button @click="memoEditCancel">取消</el-button>
<el-button type="primary" @click="memoEditSubmit">提交审核</el-button>
<el-button type="primary" @click="memoEditSubmit">提交修改</el-button>
</div>
</el-card>
</div>
</div>
<!-- 关闭商机弹窗 -->
<el-dialog
title="关闭商机"
:visible.sync="closeBusiStore.isShow"
class="audioBusiness"
width="500px">
<el-form :model="closeBusiStore" label-width="100px">
<el-form-item label="关闭原因">
<el-input
v-model="closeBusiStore.reason"
type="textarea"
:rows="4"
placeholder="请输入关闭商机原因"
></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeBusiStore.isShow = false">取消</el-button>
<el-button type="primary" @click="closeBusiSubmit">提交</el-button>
</div>
</el-dialog>
<!-- 商机成单弹窗 -->
<el-dialog
title="标记成单"
:visible.sync="dealBusiStore.isShow"
class="audioBusiness"
width="500px">
<el-form :model="dealBusiStore" label-width="100px">
<el-form-item label="备注">
<el-input
v-model="dealBusiStore.remark"
type="textarea"
:rows="4"
placeholder="请输入备注"
></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dealBusiStore.isShow = false">取消</el-button>
<el-button type="primary" @click="dealBusiSubmit">提交</el-button>
</div>
</el-dialog>
<!-- 审核弹窗 -->
<el-dialog
title="商机审核"
......@@ -279,12 +349,12 @@
<el-form :model="audioBusiStore" label-width="100px">
<el-form-item label="审核结果" required>
<el-radio-group v-model="audioBusiStore.result">
<el-radio label="approved">审核通过</el-radio>
<el-radio label="rejected">审核不通过</el-radio>
<el-radio label="1">审核通过</el-radio>
<el-radio label="0">审核不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="audioBusiStore.result === 'rejected'" label="不通过理由" required>
<el-form-item v-if="audioBusiStore.result == '0'" label="不通过理由" required>
<el-input
v-model="audioBusiStore.reason"
type="textarea"
......@@ -299,28 +369,27 @@
</div>
</el-dialog>
<!-- 新增商机弹窗 -->
<!-- 分配商机弹窗 -->
<el-dialog
title="分配商机"
:visible.sync="assignBusiStore.isShow"
width="500px">
<div style="margin-bottom: 10px;">请输入营销人员工号来分配此商机,系统将自动显示对应的营销人员信息。</div>
<el-form :model="newOpportunity" :rules="opportunityRules" ref="opportunityForm" label-width="120px">
<el-form-item label="选择营销人员" prop="businessType" required>
<el-select v-model="assignBusiStore.person" placeholder="请选择营销人员" style="width: 100%;">
<el-option label="宽带新装" value="宽带新装"></el-option>
<el-option label="宽带续费" value="宽带续费"></el-option>
<el-option label="宽带提速" value="宽带提速"></el-option>
<el-option label="ITV新装" value="ITV新装"></el-option>
<el-option label="套餐升级" value="套餐升级"></el-option>
<el-option label="家庭组网" value="家庭组网"></el-option>
<el-option label="智能家居" value="智能家居"></el-option>
<div style="margin-bottom: 10px;">请输入支撑人员工号来分配此商机,系统将自动显示对应的支撑人员信息。</div>
<el-form ref="opportunityForm" label-width="120px">
<el-form-item label="选择支撑人员" prop="marketingStaffId">
<el-select v-model="assignBusiStore.marketingStaffId" placeholder="请选择支撑人员" style="width: 100%;">
<el-option
v-for="item in yxPersonList"
:key="item.id"
:label="item.personnelTypeName"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="文字描述" prop="description">
<el-input
v-model="assignBusiStore.des"
v-model="assignBusiStore.description"
type="textarea"
:rows="3"
placeholder="请输入商机描述信息"
......@@ -339,104 +408,231 @@
<script>
// 商机状态映射
const statusMap = {
'assigned': { label: '待跟进', type: 'info' },
'following': { label: '跟进中', type: 'warning' },
'pending_review': { label: '成单待审核', type: 'primary' },
'completed': { label: '已成单', type: 'success' },
'closed': { label: '已关闭', type: 'info' }
'1': { label: '待跟进', type: 'info' },
'2': { label: '跟进中', type: 'warning' },
'3': { label: '成单待审核', type: 'primary' },
'4': { label: '已成单', type: 'success' },
'5': { label: '已关闭', type: 'info' }
}
// 模拟商机数据
const mockOpportunity = {
id: 'OP202500001',
createTime: '2025-09-27 10:30:00',
customerAddress: '南京市玄武区中山路123号',
installerId: 'INS001',
installerName: '王师傅',
installerPhone: '13987654321',
tags: ['网络升级', '家庭安防'],
status: 'following',
assignedTo: 'SALE001',
assignedToName: '张营销',
lastFollowTime: '2025-09-28 14:20:00',
customerPhone: '138****5678',
description: '用户反馈家中网络卡顿,希望升级宽带套餐,同时对家庭监控摄像头很感兴趣',
region: '南京市玄武区',
gridName: 'A网格'
}
// 模拟跟进记录
const mockActivities = [
{
content: '商机创建',
timestamp: '2025-09-27 10:30:00',
type: 'primary',
operator: '王师傅'
},
{
content: '首次跟进,联系客户确认需求',
timestamp: '2025-09-27 15:45:00',
type: 'success',
operator: '张营销'
},
{
content: '预约上门时间',
timestamp: '2025-09-28 09:30:00',
type: 'success',
operator: '张营销'
},
{
content: '上门服务完成,客户有意向办理套餐升级',
timestamp: '2025-09-28 14:20:00',
type: 'success',
operator: '张营销'
}
]
export default {
name: 'OpportunityDetail',
data() {
return {
opportunity: mockOpportunity,
activities: mockActivities,
// 管理员备注相关数据
newNote: '',
audioBusiStore:{
isShow: false,
result: '',
reason: ''
},
assignBusiStore:{
isShow: false,
person: '',
des: ''
},
memoStore:{
isEdit: false,
originValue: '',
value: ''
}
picArr: [
'https://testznzl.lgyzpt.com/activity/zjbPc/static/img/ad61df2f28f6b51d5f386829473ab1b592fd14e0.47afcfd0.png',
'https://testznzl.lgyzpt.com/activity/zjbPc/static/img/1b66793397a66bf54212d266505eb98e3377a354.fb5fc9cc.png'
],
opportunity: {},
followList: [],
closeBusiStore:{
isShow: false,
reason: ''
},
yxPersonList: [],
audioBusiStore:{
isShow: false,
result: '',
reason: ''
},
memoStore:{
isEdit: false,
value: ''
},
assignBusiStore:{
isShow: false,
marketingStaffId: '',
description: ''
},
dealBusiStore:{
isShow: false,
remark: ''
},
audioEventFlag: false
}
},
created() {
this.queryDetail()
this.queryFollow()
this.queryYxPerson()
},
methods: {
getAddressShow(value){
if(!value){
return '--'
}
return value.split(":")[1]
},
queryYxPerson(){
this.apiReq.queryAllPerson({
pageSize: 20,
pageNum: 1,
personnelTypes: [2]
}).then(res=>{
if(res.code == 200){
this.yxPersonList = res.data.records
}
})
},
closeBusiSubmit(){
if(!this.closeBusiStore.reason){
this.$message.error('请输入关闭原因')
return
}
this.apiReq.closeBusi({
opportunityId: this.opportunity.id,
reason: this.closeBusiStore.reason
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('关闭成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.queryDetail()
__this.queryFollow()
__this.closeBusiStore.isShow = false
}
});
}else{
this.$message.error(res.message)
}
})
},
queryFollow(){
this.apiReq.queryBusiFollowList({
opportunityId: this.$route.params.id,
pageNum: 1,
pageSize: 1000
}).then(res=>{
if(res.code == 200){
let __this = this
res.data.records.forEach(item=>{
item.creatTime = __this.timeRender(item.creatTime)
if(item.followImages){
item.imgArr = item.followImages.split(',')
}else{
item.imgArr = []
}
})
this.followList = res.data.records
}
})
},
handleAudio(){
if(this.audioEventFlag){
return
}
this.audioEventFlag = true
// 获取所有需要互斥的音频
const audios = [...document.querySelectorAll('.single-audio')]
let current = null
audios.forEach(audio => {
audio.addEventListener('play', () => {
if (current && current !== audio) {
current.pause()
current.currentTime = 0 // 如需“停完归零”就保留
}
current = audio
})
audio.addEventListener('ended', () => { current = null })
// audio.addEventListener('pause', () => { current = null })
})
},
timeRender(time){
return this.common.detailTime(new Date(time).getTime())
},
queryDetail(){
this.apiReq.queryBusiDetail({
opportunityId: this.$route.params.id
}).then(res=>{
if(res.code == 200){
if(res.data.voiceDescriptionUrl){
res.data.audioArr = res.data.voiceDescriptionUrl.split(',')
}
this.opportunity = res.data
this.memoStore.value = res.data.adminRemark
this.handleAudio()
}else{
this.$alert(res.$message, '温馨提示', {
confirmButtonText: '确定',
callback: () => {
history.go(-1)
}
});
}
})
},
memoEditCancel(){
let d = this.memoStore
d.value = d.originValue
d.value = this.opportunity.adminRemark
d.isEdit = false
},
memoEditSubmit(){
if(!this.memoStore.value){
this.$message.error('请输入备注')
return
}
this.apiReq.updateRemark({
opportunityId: this.opportunity.id,
adminRemark: this.memoStore.value
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('编辑成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.queryDetail()
__this.queryFollow()
__this.memoStore.isEdit = false
}
});
}else{
this.$message.error(res.message)
}
})
},
audioBusiSubmit(){
let ad = this.audioBusiStore
if(ad.result==0 && !ad.reason){
this.$message.error('请输入不通过理由')
return
}
this.apiReq.audioBusi({
auditStatus: ad.result,
opportunityId: this.opportunity.id,
reason: ad.reason
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('审核完成', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.queryDetail()
__this.queryFollow()
ad.isShow = false
}
});
}else{
this.$message.error(res.message)
}
})
},
goBack() {
......@@ -449,21 +645,79 @@ export default {
return statusMap[status]?.label || status
},
assignBusi() {
this.assignBusiStore.des = ''
this.assignBusiStore.person = ''
this.assignBusiStore.description = ''
this.assignBusiStore.marketingStaffId = ''
this.assignBusiStore.isShow = true
},
makeOrder(){
this.$message.success('标记成单')
assingBusiSubmit(){
let p = this.assignBusiStore
if(!p.marketingStaffId){
this.$message.success('请选择支撑人员')
return
}
this.apiReq.reassignBusi({
description: p.description,
marketingStaffId: p.marketingStaffId,
opportunityId: this.opportunity.id
}).then(res=>{
if(res.code == 200){
let __this = this
this.$alert('分配成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.queryDetail()
__this.queryFollow()
__this.assignBusiStore.isShow = false
}
});
}else{
this.$message.error(res.message)
}
})
},
dealBusi(){
this.dealBusiStore.remark = ''
this.dealBusiStore.isShow = true
},
dealBusiSubmit(){
if(!this.dealBusiStore.remark){
this.$message.error('请输入备注')
return
}
this.apiReq.dealBusi({
opportunityId: this.opportunity.id,
adminRemark: this.dealBusiStore.remark
}).then(res=>{
if(res.code == '200'){
let __this = this
this.$alert('标记成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.queryDetail()
__this.queryFollow()
__this.dealBusiStore.isShow = false
}
});
}else{
this.$message.error(res.message)
}
})
},
audioBusi() {
this.audioBusiStore.result = ''
this.audioBusiStore.reason = ''
this.audioBusiStore.isShow = true
},
shutBusi() {
this.$message.success('关闭商机')
this.closeBusiStore.reason = ''
this.closeBusiStore.isShow = true
}
}
}
......@@ -518,6 +772,12 @@ export default {
.info-value {
flex: 1;
color: #303133;
audio{
display: block;
width: 100%;
height: 40px;
margin: 20px 0;
}
}
}
......
......@@ -74,29 +74,6 @@
<!-- 筛选条件 -->
<el-card class="filter-card">
<div class="filter-header">
<h3 class="filter-title">筛选条件</h3>
<div class="filter-actions">
<el-button size="small" @click="handleFilter">
<i class="el-icon-filter"></i>
查询
</el-button>
<el-button size="small" @click="handleExport" type="default">
<i class="el-icon-download"></i>
导出数据
</el-button>
<el-button
v-if="hasFilters"
size="small"
@click="clearFilters"
type="default"
>
<i class="el-icon-close"></i>
清除筛选
</el-button>
</div>
</div>
<div class="filter-content">
<el-row :gutter="24">
<el-col :span="3">
......@@ -104,6 +81,7 @@
<el-input
v-model="formData.customerPhone"
placeholder="用户账号"
clearable
style="padding-left: 0;"
/>
</div>
......@@ -112,6 +90,7 @@
<div class="search-wrapper">
<el-input
v-model="formData.maintenanceStaffName"
clearable
placeholder="师傅名字"
style="padding-left: 0;"
/>
......@@ -121,7 +100,8 @@
<div class="search-wrapper">
<el-input
v-model="formData.marketingStaffName"
placeholder="营销人员名字"
clearable
placeholder="支撑人员名字"
style="padding-left: 0;"
/>
</div>
......@@ -130,6 +110,7 @@
<el-col :span="7">
<el-date-picker
v-model="formData.createTime"
clearable
type="daterange"
range-separator="至"
start-placeholder="开始日期"
......@@ -144,8 +125,7 @@
<el-select
v-model="formData.status"
placeholder="选择状态"
clearable
>
clearable>
<el-option label="全部状态" value="all"></el-option>
<el-option
v-for="item in statusStore"
......@@ -169,9 +149,9 @@
clearable>
<el-option
v-for="item in addressStore.cityArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</el-col>
......@@ -184,9 +164,9 @@
clearable>
<el-option
v-for="item in addressStore.countyArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.areaCode"
:label="item.areaName"
:value="item.areaCode"
></el-option>
</el-select>
</el-col>
......@@ -198,14 +178,27 @@
clearable>
<el-option
v-for="item in addressStore.gridArr"
:key="item.value"
:label="item.name"
:value="item.value"
:key="item.gridCode"
:label="item.gridName"
:value="item.gridCode"
></el-option>
</el-select>
</el-col>
<el-col :span="3">
<el-button size="small" @click="handleFilter">
<i class="el-icon-filter"></i>
查询
</el-button>
<!-- <el-button size="small" @click="handleExport" type="default" disabled>
<i class="el-icon-download"></i>
导出数据
</el-button> -->
</el-col>
</el-row>
</div>
</el-card>
<!-- 商机列表 -->
......@@ -215,7 +208,7 @@
<div class="list-actions">
<el-button
size="small"
@click="isCreateDialogOpen = true"
@click="addNewBusi"
type="primary"
>
<i class="el-icon-plus"></i>
......@@ -225,15 +218,15 @@
</div>
<div class="list-content">
<el-table
<el-table
:data="tableData" border
v-loading="loading">
<el-table-column prop="id" label="商机ID" width="150"></el-table-column>
<el-table-column prop="createTime" label="创建时间" :formatter="timeRender" width="180"></el-table-column>
<el-table-column prop="customerAddress" label="客户地址" width="230" show-overflow-tooltip></el-table-column>
<el-table-column prop="customerAddress" label="客户地址" width="230" :formatter="addressRender" show-overflow-tooltip></el-table-column>
<el-table-column prop="customerPhone" label="用户账号" width="120"></el-table-column>
<el-table-column prop="maintenanceStaffName" label="装维师傅姓名" width="120"></el-table-column>
<el-table-column prop="marketingStaffName" label="营销人员姓名" width="120"></el-table-column>
<el-table-column prop="marketingStaffName" label="支撑人员姓名" width="120"></el-table-column>
<el-table-column label="商机标签" width="250">
<template slot-scope="scope">
<el-tag
......@@ -252,7 +245,7 @@
<el-table-column label="操作" min-width="200">
<template slot-scope="scope">
<el-button type="text" size="small" @click="checkDetail(scope.row)">查看详情</el-button>
<el-button type="text" size="small" @click="checkAudio(scope.row)">审核</el-button>
<el-button type="text" size="small" @click="checkAudio(scope.row)" :disabled="scope.row.status!=3">审核</el-button>
</template>
</el-table-column>
</el-table>
......@@ -276,32 +269,70 @@
<!-- 新增商机弹窗 -->
<el-dialog
title="新增商机"
:visible.sync="isCreateDialogOpen"
width="500px">
<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>
:visible.sync="newOpportunity.isShow"
width="800px">
<el-form label-width="120px">
<el-form-item label="用户账号" prop="customerPhone">
<el-input v-model="newOpportunity.customerPhone" placeholder="请输入用户账号"></el-input>
</el-form-item>
<el-form-item label="客户地址" prop="customerAddress" required>
<el-input v-model="newOpportunity.customerAddress" placeholder="请输入客户地址"></el-input>
<el-form-item label="客户地址" prop="city">
<template v-if="newOpportunity.cityArr.length>0">
<el-select
v-model="newOpportunity.city"
placeholder="选择地市"
@change="addCityChange"
style="margin-right: 10px"
clearable>
<el-option
v-for="item in newOpportunity.cityArr"
:key="item.code"
:label="item.name"
:value="item.code"
></el-option>
</el-select>
</template>
<template v-if="newOpportunity.countyArr.length>0">
<el-select
v-model="newOpportunity.county"
placeholder="选择区县"
@change="addCountyChange"
style="margin-right: 10px"
clearable>
<el-option
v-for="item in newOpportunity.countyArr"
:key="item.code"
:label="item.name"
:value="item.code"
></el-option>
</el-select>
</template>
<template v-if="newOpportunity.streetsArr.length>0">
<el-select
v-model="newOpportunity.streets"
placeholder="选择街道"
@change="addStreetsChange"
clearable>
<el-option
v-for="item in newOpportunity.streetsArr"
:key="item.code"
:label="item.name"
:value="item.code"
></el-option>
</el-select>
</template>
<el-input v-if="newOpportunity.streets" v-model="newOpportunity.detail" placeholder="请输入详细地址" style="margin-top: 10px;"></el-input>
</el-form-item>
<el-form-item label="装维人员工号" prop="installerWorkId" required>
<el-input v-model="newOpportunity.installerWorkId" placeholder="请输入装维人员工号"></el-input>
<el-form-item label="装维人员工号" prop="maintenanceStaffNo">
<el-input v-model="newOpportunity.maintenanceStaffNo" placeholder="请输入装维人员工号"></el-input>
</el-form-item>
<el-form-item label="业务类型" prop="businessType">
<el-select v-model="newOpportunity.businessType" placeholder="请选择业务类型" style="width: 100%;">
<el-option label="宽带新装" value="宽带新装"></el-option>
<el-option label="宽带续费" value="宽带续费"></el-option>
<el-option label="宽带提速" value="宽带提速"></el-option>
<el-option label="ITV新装" value="ITV新装"></el-option>
<el-option label="套餐升级" value="套餐升级"></el-option>
<el-option label="家庭组网" value="家庭组网"></el-option>
<el-option label="智能家居" value="智能家居"></el-option>
<el-form-item label="业务类型" prop="tagIds">
<el-select v-model="newOpportunity.tagIds" placeholder="请选择业务类型" style="width: 100%;" multiple>
<el-option v-for="item in busiLabelStore" :label="item.tagName" :value="item.id" :disabled="isTagCanCho(item)"></el-option>
</el-select>
</el-form-item>
<el-form-item label="文字描述" prop="description">
<el-form-item label="文字描述" prop="textDescription">
<el-input
v-model="newOpportunity.description"
v-model="newOpportunity.textDescription"
type="textarea"
:rows="3"
placeholder="请输入商机描述信息"
......@@ -309,8 +340,8 @@
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isCreateDialogOpen = false">取消</el-button>
<el-button type="primary" @click="handleCreateOpportunity">提交</el-button>
<el-button @click="newOpportunity.isShow = false">取消</el-button>
<el-button type="primary" @click="addBusiSubmit">提交</el-button>
</div>
</el-dialog>
......@@ -322,12 +353,12 @@
<el-form :model="auditForm" label-width="100px">
<el-form-item label="审核结果" required>
<el-radio-group v-model="auditForm.result">
<el-radio label="approved">审核通过</el-radio>
<el-radio label="rejected">审核不通过</el-radio>
<el-radio label="1">审核通过</el-radio>
<el-radio label="0">审核不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="auditForm.result === 'rejected'" label="不通过理由" required>
<el-form-item v-if="auditForm.result == '0'" label="不通过理由" required>
<el-input
v-model="auditForm.reason"
type="textarea"
......@@ -360,7 +391,7 @@ const statusMap = {
const opportunityTags = [
'网络升级', '家庭安防', '智能家居', '宽带套餐', '移动套餐', '融合套餐', '企业业务'
]
// 营销人员列表
// 支撑人员列表
const mockSalesPersons = [
{ id: 'SALE001', name: '张营销', gridName: 'A网格', phone: '13812345001' },
{ id: 'SALE002', name: '陈营销', gridName: 'B网格', phone: '13812345002' },
......@@ -373,33 +404,25 @@ export default {
name: 'OpportunityManagement',
data() {
return {
busiLabelStore: [],
formData:{
customerPhone: '18277777766',
customerPhone: '',
maintenanceStaffName: '',
marketingStaffName: '',
createTime: '',
status: '',
areaCode: ''
},
addressStore:{
city: '',
cityName: '',
cityArr: this.storesData.addressData,
cityArr: '',
county: '',
countyName: '',
countyArr: [],
grid: '',
gridName: '',
gridArr: []
},
getData:{
city: '',
cityName: '',
county: '',
countyName: '',
grid: '',
gridName: '',
},
statsStore:{},
statusStore:[],
......@@ -410,23 +433,27 @@ export default {
total: 0
},
newOpportunity: {
customerAccount: '',
address: {
city: '',
cityName: '',
cityArr: this.storesData.choAddressData,
county: '',
countyName: '',
countyArr: [],
streets: '',
streetsName: '',
streetsArr: [],
detail: ''
},
installerWorkId: '',
businessType: '',
description: ''
isShow: false,
customerPhone: '',
city: '',
cityName: '',
cityArr: this.storesData.choAddressData,
county: '',
countyName: '',
countyArr: [],
streets: '',
streetsName: '',
streetsArr: [],
detail: '',
maintenanceStaffNo: '',
tagIds: [],
opportunityType: '',
textDescription: ''
},
busiLabelStore: [],
loading: false,
statusMap,
......@@ -438,19 +465,6 @@ export default {
selectedStatus: '',
selectedTag: '',
dateRange: [],
isCreateDialogOpen: false,
opportunityRules: {
customerAccount: [
{ required: true, message: '请输入用户账号', trigger: 'blur' }
],
customerAddress: [
{ required: true, message: '请输入客户地址', trigger: 'blur' }
],
installerWorkId: [
{ required: true, message: '请输入装维人员工号', trigger: 'blur' }
]
},
isAuditDialogOpen: false,
auditForm: {
result: '',
......@@ -463,33 +477,79 @@ export default {
}
},
created(){
this.setAddressShow()
let pa = JSON.parse(localStorage.getItem('accountInfo'))
this.getData.city = pa.cityCode||''
this.getData.county = pa.countyCode||''
this.getData.grid = pa.gridCode||''
this.queryArea()
this.queryBusi()
this.queryStatistics()
this.queryStatus()
this.queryAllBusiLabel()
this.apiReq.queryAllArea({
areaLevel: 1,
parentAreaCode: ''
})
},
computed: {
...mapGetters(['user']),
// 是否有筛选条件
hasFilters() {
return this.searchTerm ||
this.selectedRegion ||
this.selectedStatus ||
this.selectedTag ||
(this.dateRange && this.dateRange.length > 0)
}
},
methods: {
addressRender(row,column){
let value = row[column.property]
if(!value){
return '--'
}
return value.split(":")[1]
},
queryArea(){
this.apiReq.queryLevelAllArea({
areaLevel: 2,
parentAreaCode: '320000'
}).then(res=>{
if(res.code == 200){
this.addressStore.cityArr = res.data
this.setAddressShow()
}
})
},
isTagCanCho(row){
let arr = this.newOpportunity.tagIds
if(arr.length == 0){
return false
}
if(arr.includes(1) || arr.includes(2)){
this.newOpportunity.opportunityType = 2
if(row.id!=1 && row.id!=2){
return true
}else{
return false
}
}else{
this.newOpportunity.opportunityType = 1
if(row.id==1 || row.id==2){
return true
}else{
return false
}
}
},
addNewBusi(){
let nd = this.newOpportunity
nd.customerPhone = ''
nd.city = ''
nd.cityName = ''
nd.county = ''
nd.countyName = ''
nd.streets = ''
nd.streetsName = ''
nd.maintenanceStaffNo = ''
nd.tagIds = ''
nd.textDescription = ''
nd.isShow = true
},
queryAllBusiLabel(){
this.apiReq.queryBusiLabel({
this.apiReq.queryBusiLabelList({
pageNum: 1,
pageSize: 20,
}).then(res=>{
......@@ -510,22 +570,11 @@ export default {
if(gd.city){
ad.city = gd.city
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
}
})
this.cityChange(ad.city)
if(gd.county){
ad.county = gd.county
ad.countyArr.forEach(item=>{
if(item.value == ad.county){
ad.gridArr = item.children
}
})
if(ad.gridArr.length == 1){
gd.grid = 'moren'
}
this.countyChange(ad.county)
if(gd.grid){
ad.grid = gd.grid
......@@ -540,9 +589,12 @@ export default {
ad.county = ''
ad.grid = ''
ad.cityArr.forEach(item=>{
if(item.value == ad.city){
ad.countyArr = item.children
this.apiReq.queryLevelAllArea({
areaLevel: 3,
parentAreaCode: value
}).then(res=>{
if(res.code == 200){
ad.countyArr = res.data
}
})
},
......@@ -551,12 +603,41 @@ export default {
ad.county = value
ad.grid = ''
this.apiReq.queryGridList({
areaCode:value
}).then(res=>{
if(res.code == 200){
ad.gridArr = res.data
}
})
},
addCityChange(value){
let ad = this.newOpportunity
ad.city = value
ad.cityName = ad.cityArr.find(item => item.code == value).name
ad.cityArr.forEach(item=>{
if(item.code == ad.city){
ad.countyArr = item.districts
}
})
},
addCountyChange(value){
let ad = this.newOpportunity
ad.county = value
ad.countyName = ad.countyArr.find(item => item.code == value).name
ad.countyArr.forEach(item=>{
if(item.value == ad.county){
ad.gridArr = item.children
if(item.code == ad.county){
ad.streetsArr = item.streets
}
})
},
addStreetsChange(value){
let ad = this.newOpportunity
ad.streets = value
ad.streetsName = ad.streetsArr.find(item => item.code == value).name
},
queryBusi(){
let d = this.formData
......@@ -569,27 +650,28 @@ export default {
}
this.apiReq.queryAllBusi({
pageNum: 1,
pageSize: 20,
pageNum: this.pageStore.currentPage,
pageSize: this.pageStore.pageSize,
areaCode: this.addressStore.county || this.addressStore.city,
gridCode: this.addressStore.grid,
...d
}).then(res=>{
if(res.code == 200){
this.tableData = res.data.records
this.pageStore.total = res.total
this.pageStore.total = res.data.total
}else{
this.$message.error(res.message)
}
})
},
handleSizeChange(size) {
this.pageSize = size
this.currentPage = 1
this.queryBusi()
this.pageStore.pageSize = size
this.handleFilter()
},
handleCurrentChange(page) {
this.currentPage = page
this.pageStore.currentPage = page
this.queryBusi()
},
......@@ -607,8 +689,50 @@ export default {
}
})
},
addBusiSubmit(){
let nd = this.newOpportunity
if(!nd.customerPhone){
this.$message.error('请输入用户账号')
return
}
if(!nd.city || !nd.county || !nd.streets || !nd.detail){
this.$message.error('请补全客户地址信息')
return
}
let address = nd.city+'|'+nd.county+'|'+nd.streets+":"+nd.cityName+nd.countyName+nd.streetsName+nd.detail
if(!nd.maintenanceStaffNo){
this.$message.error('请输入装维人员工号')
return
}
if(!nd.tagIds){
this.$message.error('请选择业务类型')
return
}
this.apiReq.createBusi({
customerAddress: address,
...nd
}).then(res=>{
let __this = this
if(res.code == 200){
this.$alert('添加成功', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
__this.handleFilter()
__this.newOpportunity.isShow = false
}
});
}else{
this.$message.error(res.message)
}
})
},
......@@ -625,7 +749,7 @@ export default {
},
handleFilter() {
// 重新计算筛选结果
this.currentPage = 1
this.pageStore.currentPage = 1
this.queryBusi()
},
......@@ -643,19 +767,6 @@ export default {
checkAudio(row){
this.handleOpenAudit(row.id)
},
handleCreateOpportunity() {
this.$refs.opportunityForm.validate((valid) => {
if (valid) {
// 验证必填字段
if (!this.newOpportunity.customerAccount || !this.newOpportunity.customerAddress || !this.newOpportunity.installerWorkId) {
this.$message.error('请填写所有必填字段')
return
}
}
})
},
handleExport() {
// 模拟导出功能
this.$message.success('商机数据导出成功')
......@@ -672,22 +783,34 @@ export default {
return
}
if (this.auditForm.result === 'rejected' && !this.auditForm.reason.trim()) {
if (this.auditForm.result == '0' && !this.auditForm.reason.trim()) {
this.$message.error('审核不通过时必须填写理由')
return
}
// 模拟审核操作
const resultText = this.auditForm.result === 'approved' ? '通过' : '不通过'
this.$message.success(`商机 ${this.auditOpportunityId} 审核${resultText}`)
// 重置状态并关闭对话框
this.isAuditDialogOpen = false
this.auditOpportunityId = null
this.auditForm = {
result: '',
reason: ''
this.apiReq.audioBusi({
auditStatus: this.auditForm.result,
opportunityId: this.auditOpportunityId,
reason: this.auditForm.reason
}).then(res=>{
if(res.code == 200){
let __this = this
this.$alert('审核完成', '温馨提示', {
confirmButtonText: '确定',
callback: () => {
// 重置状态并关闭对话框
__this.isAuditDialogOpen = false
__this.auditOpportunityId = null
__this.auditForm = {
result: '',
reason: ''
}
}
});
}else{
this.$message.error(res.message)
}
})
}
}
}
......@@ -695,6 +818,10 @@ export default {
<style lang="scss" scoped>
.opportunity-management {
.el-button.el-button--small{
padding: 13px 12px;
}
.page-header {
margin-bottom: 24px;
......
......@@ -6,14 +6,14 @@
</div>
<!-- 人员管理页面特殊处理:显示业务类型Tab -->
<div v-if="activeTab === 'personnel'" class="personnel-management">
<div v-if="activeTab == 'personnel'" class="personnel-management">
<el-tabs v-model="businessTab" @tab-click="handleBusinessTabClick">
<el-tab-pane label="常规业务" name="regular"></el-tab-pane>
<el-tab-pane label="政企业务" name="enterprise" v-if="hasEnterprisePermission"></el-tab-pane>
</el-tabs>
<!-- 常规业务 -->
<div v-if="businessTab === 'regular'">
<div v-if="businessTab == 'regular'">
<el-card>
<div class="card-content">
<PersonnelManagement />
......@@ -22,7 +22,7 @@
</div>
<!-- 政企业务 -->
<div v-if="businessTab === 'enterprise' && hasEnterprisePermission">
<div v-if="businessTab == 'enterprise' && hasEnterprisePermission">
<el-card>
<div class="card-content">
<EnterprisePersonnelManagement />
......@@ -32,14 +32,14 @@
</div>
<!-- 商机管理配置 -->
<div v-else-if="activeTab === 'tags'" class="opportunity-config">
<div v-else-if="activeTab == 'tags'" class="opportunity-config">
<el-tabs v-model="configTab" @tab-click="handleConfigTabClick">
<el-tab-pane label="商机标签" name="tags"></el-tab-pane>
<el-tab-pane label="商机关闭" name="reasons"></el-tab-pane>
</el-tabs>
<!-- 商机标签 -->
<div v-if="configTab === 'tags'">
<div v-if="configTab == 'tags'">
<el-card>
<div class="card-content">
<TagManagement />
......@@ -48,7 +48,7 @@
</div>
<!-- 商机关闭原因 -->
<div v-if="configTab === 'reasons'">
<div v-if="configTab == 'reasons'">
<el-card>
<div class="card-content">
<CloseReasonManagement />
......@@ -58,7 +58,7 @@
</div>
<!-- 账号管理 -->
<div v-else-if="activeTab === 'accounts'">
<div v-else-if="activeTab == 'accounts'">
<el-card>
<div class="card-content">
<AccountManagement />
......@@ -98,9 +98,10 @@ export default {
}
},
computed: {
...mapGetters(['user']),
hasEnterprisePermission() {
return this.user?.role === 'city_admin' || this.user?.role === 'province_admin'
let accountInfo = JSON.parse(localStorage.getItem('accountInfo'))
return !accountInfo.countyCode && !accountInfo.gridCode
},
activeTabLabel() {
const tabLabels = {
......
......@@ -5,12 +5,12 @@ module.exports = {
publicPath: './',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
lintOnSave: process.env.NODE_ENV == 'development',
productionSourceMap: false,
devServer: {
proxy: {
'/compass': {
target: 'http://39.107.104.220:8899',
target: 'https://testznzl.lgyzpt.com/',
//target: 'https://hall.51xinpai.cn',
changeOrigin: true,
pathRewrite: {
......@@ -27,7 +27,8 @@ module.exports = {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
},
devtool: 'source-map'
},
chainWebpack: config => {
// 配置SVG图标
......
......@@ -12,6 +12,11 @@
<audio ref="audioObj" playsinline></audio>
<!-- <div class="topBut" v-if="isAlone">
<div class="left" @click="goZj">质检模拟</div>
<div class="right" @click="loginOut">退出</div>
</div> -->
<!-- 联系方式 -->
<div class="section" data-node-id="79:2566">
<div class="section-card" data-node-id="6:249">
......@@ -20,7 +25,7 @@
<span class="contact-text" data-node-id="6:259">{{ contactPhone }}</span>
<button class="edit-button" data-node-id="6:263" @click="editContact">修改</button>
</div>
<input class="phoneInput" v-else type="text" placeholder="请输入手机号">
<input class="phoneInput" v-model="contactPhone" v-else type="text" placeholder="请输入手机号">
</div>
</div>
......@@ -37,7 +42,7 @@
</div>
<div class="addressLi">
<div class="name">详细地址</div>
<input type="text" placeholder="门牌号、楼栋号">
<input type="text" placeholder="门牌号、楼栋号" v-model="detailAddress">
</div>
</div>
</div>
......@@ -51,10 +56,11 @@
v-for="type in businessTypes"
:key="type.id"
:class="['tag-button', { active: type.selected }]"
:style="{color: type.ifCho?'#33':'#CECECE'}"
:data-node-id="type.nodeId"
@click="selectBusinessType(type.id)"
>
{{ type.name }}
{{ type.tagName }}
</div>
</div>
</div>
......@@ -67,8 +73,11 @@
<div class="cordList">
<div class="cordLi" v-for="(item,index) in recordingUrlArr">
<div class="left" @click="playAudio(index)">
<img class="pause" src="images/pause.png" alt="">
<img class="voice" src="images/voice.png" alt="">
<img v-if="item.isPlay" class="pause" src="images/play.png" alt="">
<img v-else class="pause" src="images/pause.png" alt="">
<img class="voice" src="images/voice.png" alt="">
<div class="time">{{item.time}}</div>
</div>
<img class="close" src="images/close.png" @click="removeAudio(index)">
......@@ -134,14 +143,20 @@
<div v-else class="submit-button" data-node-id="6:498" @click="submitBusiness">
提交商机
</div>
<div class="loginOut" @click="loginOut">
<img src="images/loginout.png" alt="">
<div>退出</div>
</div>
<!-- 底部导航 -->
<div class="bottom-nav" v-if="isAlone">
<div class="nav-item collect-business">
<img class="nav-icon" src="images/collect-icon.svg" alt="收集商机">
<div class="nav-item collect-business active">
<img class="nav-icon" src="images/collect-icon-active.png" alt="收集商机">
<span class="nav-text" data-node-id="355:528">收集商机</span>
</div>
<div class="nav-item all-business active">
<img class="nav-icon" src="images/business-icon.svg" alt="收集商机">
<div class="nav-item all-business" @click="navigateToList">
<img class="nav-icon" src="images/business-icon.png" alt="收集商机">
<span class="nav-text" data-node-id="355:532">全部商机</span>
</div>
</div>
......@@ -197,9 +212,11 @@
</div>
<!-- 引入Vue.js -->
<script src="https://cdn.jsdelivr.net/npm/lamejs@1.2.1/lame.min.js"></script>
<script src="js/axios.min.js"></script>
<script src="js/vue.min.js"></script>
<script src="js/util.js"></script>
<script src="js/addressData.js"></script>
<script src="js/addBusi.js"></script>
<script src="js/addBusi.js?123211"></script>
</body>
</html>
\ No newline at end of file
......@@ -11,12 +11,13 @@
<div class="app-container" :class="{'worker-con': isWorker}">
<img class="topImg" src="images/bg.png" alt="">
<audio ref="audioObj" playsinline></audio>
<!-- 状态显示 -->
<div class="status-section" data-node-id="294:2278">
<div class="status-content">
<h1 class="status-title" data-node-id="294:2279">{{ businessDetail.status }}</h1>
<p class="status-desc" data-node-id="294:2280">{{ businessDetail.statusDescription }}</p>
<h1 class="status-title" data-node-id="294:2279">{{ followNewInfo.statusName }}</h1>
<p class="status-desc" data-node-id="294:2280">{{ followNewInfo.des }}</p>
</div>
</div>
......@@ -28,12 +29,12 @@
<!-- 客户地址 -->
<div class="info-row" data-node-id="294:2286">
<span class="info-label" data-node-id="294:2287">客户地址</span>
<span class="info-value" data-node-id="294:2288">{{ businessDetail.customerAddress }}</span>
<span class="info-value" data-node-id="294:2288">{{ getAddressShow(businessDetail.customerAddress)}}</span>
</div>
<!-- 客户账号 -->
<div class="info-row" data-node-id="294:2289">
<span class="info-label" data-node-id="294:2290">客户账号</span>
<span class="info-value" data-node-id="294:2291">{{ businessDetail.customerAccount }}</span>
<span class="info-value" data-node-id="294:2291">{{ businessDetail.maintenanceStaffPhone }}</span>
</div>
<!-- 客户电话 -->
<div class="info-row" data-node-id="294:2292">
......@@ -43,41 +44,58 @@
<img class="copy-icon" src="images/copy.png" data-node-id="294:2296" @click="copyPhone"></img>
</div>
</div>
<!-- 装维师傅 -->
<div class="info-row" data-node-id="294:2298" v-if="!isWorker">
<span class="info-label" data-node-id="294:2299">装维师傅 </span>
<div class="phone-container" data-node-id="294:2300">
<span class="info-value" data-node-id="294:2301">{{ businessDetail.maintenanceStaffPhone }}</span>
<img class="copy-icon" src="images/copy.png" data-node-id="294:2302" @click="copyMarketerPhone"></img>
</div>
</div>
<!-- 营销人员 -->
<div class="info-row" data-node-id="294:2298">
<div class="info-row" data-node-id="294:2298" v-else>
<span class="info-label" data-node-id="294:2299">营销人员</span>
<div class="phone-container" data-node-id="294:2300">
<span class="info-value" data-node-id="294:2301">{{ businessDetail.marketerPhone }}</span>
<span class="info-value" data-node-id="294:2301">{{ businessDetail.marketingStaffPhone }}</span>
<img class="copy-icon" src="images/copy.png" data-node-id="294:2302" @click="copyMarketerPhone"></img>
</div>
</div>
<!-- 营销类型 -->
<div class="info-row" data-node-id="294:2304">
<span class="info-label" data-node-id="294:2305">营销类型</span>
<div class="type-container" data-node-id="294:2306">
<div class="type-tag" data-node-id="294:2307">
<span data-node-id="294:2308">{{ businessDetail.marketingTypes[0] }}</span>
</div>
<div class="type-tag" data-node-id="294:2309">
<span data-node-id="294:2310">{{ businessDetail.marketingTypes[1] }}</span>
<div class="type-tag" v-for="item in businessDetail.tagNames">
<span >{{ item }}</span>
</div>
</div>
</div>
<!-- 提交时间 -->
<div class="info-row" data-node-id="294:2311">
<span class="info-label" data-node-id="294:2312">提交时间</span>
<span class="info-value" data-node-id="294:2313">{{ businessDetail.submitTime }}</span>
<span class="info-value" data-node-id="294:2313">{{ switchTime(businessDetail.createTime) }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<div class="action-button contact-marketer" data-node-id="294:2314" @click="callMarketer">
<div class="action-buttons" v-if="isWorker">
<div class="action-button contact-marketer" data-node-id="294:2314" @click="takeCall('yx')">
<span data-node-id="294:2316">致电营销人员</span>
</div>
<div class="action-button contact-customer" data-node-id="294:2317" @click="callCustomer" v-if="!isWorker">
</div>
<div class="action-buttons" v-else>
<div class="action-button contact-marketer" data-node-id="294:2317" @click="takeCall('customer')">
<span data-node-id="294:2319">联系客户</span>
</div>
<div class="action-button contact-customer" data-node-id="294:2314" @click="takeCall('zw')" v-if="platform='yx'">
<span data-node-id="294:2316">致电装维师傅</span>
</div>
<div class="action-button contact-customer" data-node-id="294:2314" @click="takeCall('yx')" v-else>
<span data-node-id="294:2316">致电营销人员</span>
</div>
</div>
</div>
</div>
......@@ -92,19 +110,22 @@
<!-- 多条语音记录 -->
<div class="voiceList">
<div class="voiceLi" v-for="item in '123'">
<div class="voiceLi" v-for="(item,index) in businessDetail.voiceRecords">
<div class="up">
<div class="left">
<img class="pause" src="images/pause.png" alt="">
<div class="left" @click="playAudio(index)">
<img v-if="item.isPlay" class="pause" src="images/play.png" alt="">
<img v-else class="pause" src="images/pause.png" alt="">
<img class="voice" src="images/voice.png" alt="">
<div class="time">{{item.time}}</div>
</div>
<div class="right">
<div class="right" v-if="!isWorker" @click="audioToText(index)">
<img src="images/text.png" alt="">
<div>转为文字</div>
</div>
</div>
<div class="textDiv">asdfasdfadf阿斯顿发到发送到发送到发</div>
<div class="textDiv" v-if="!isWorker && item.text">{{item.text}}</div>
</div>
</div>
</div>
......@@ -113,7 +134,7 @@
<div class="text-section">
<h3 class="subtitle" data-node-id="294:2459">文字描述</h3>
<div class="text-content" data-node-id="294:2460">
<p data-node-id="294:2461">{{ businessDetail.textDescription }}</p>
<p data-node-id="294:2461">{{ businessDetail.textDescription||'暂无' }}</p>
</div>
</div>
</div>
......@@ -124,7 +145,7 @@
<div class="notes-card">
<h2 class="section-title" data-node-id="294:2361">管理员备注</h2>
<div class="notes-content" data-node-id="294:2362">
<p data-node-id="294:2362">{{ businessDetail.adminNotes }}</p>
<p data-node-id="294:2362">{{ businessDetail.adminRemark||'暂无' }}</p>
</div>
</div>
</div>
......@@ -135,29 +156,15 @@
<h2 class="section-title" data-node-id="294:2695">处理进度</h2>
<div class="progress-content" data-node-id="294:2696">
<!-- 跟进中 -->
<div class="progress-item" v-if="businessDetail.followUpStatus" data-node-id="294:2697">
<div class="progress-item" v-for="item in followList">
<div class="circle">
<div></div>
</div>
<div class="progress-status" data-node-id="294:2698">
<h3 class="progress-title">{{ businessDetail.followUpStatus }}</h3>
<h3 class="progress-title">{{ getStatusName(item.status) }}</h3>
<div class="progress-details" data-node-id="294:2699">
<p class="progress-time">{{ businessDetail.followUpTime }}</p>
<p class="progress-desc">{{ businessDetail.followUpDescription }}</p>
</div>
</div>
</div>
<!-- 商机创建 -->
<div class="progress-item" data-node-id="294:2702">
<div class="circle cgray">
<div></div>
</div>
<div class="progress-status" data-node-id="294:2703">
<h3 class="progress-title">商机创建</h3>
<div class="progress-details" data-node-id="294:2704">
<p class="progress-time">{{ businessDetail.createTime }}</p>
<p class="progress-desc">{{ businessDetail.createDescription }}</p>
<p class="progress-time">{{ switchTime(item.createTime) }}</p>
<p class="progress-desc">{{ item.followPersonName }}:{{ item.followContent }}</p>
</div>
</div>
</div>
......@@ -166,15 +173,15 @@
</div>
<div class="botButt" v-if="!isWorker">
<div class="bbli">
<div class="bbli" @click="addBusiProcess" :style="{opacity: ifCanGj?1:0.3}">
<img src="images/genjin.png" alt="">
<div>写跟进</div>
</div>
<div class="bbli">
<div class="bbli" @click="completeBusi" :style="{opacity: ifCanCd?1:0.3}">
<img src="images/chengdan.png" alt="">
<div>转为成单</div>
</div>
<div class="bbli">
<div class="bbli" @click="closeBusi" :style="{opacity: ifCanGb?1:0.3}">
<img src="images/guanbi.png" alt="">
<div>关闭商机</div>
</div>
......@@ -186,29 +193,25 @@
<div class="title">写跟进</div>
<div class="ali">
<div class="til">跟进内容</div>
<textarea class="input" placeholder="请输入"></textarea>
<textarea v-model="gjStore.des" class="input" placeholder="请输入"></textarea>
</div>
<div class="ali">
<div class="til">上传图片(最多三张)</div>
<div class="imgList">
<div class="addImg">
<div class="addImg" v-if="gjStore.imgArr.length<3">
<img class="add" src="images/add.png" alt="">
<input type="file" @change="fileChange" accept="image/*" :disabled="gjStore.isLoading">
</div>
<div class="imgShow">
<img class="show" src="images/bg.png" alt="">
<img class="close" src="images/close.png" alt="">
</div>
<div class="imgShow">
<img class="show" src="images/bg.png" alt="">
<img class="close" src="images/close.png" alt="">
<div class="imgShow" v-for="(item,index) in gjStore.imgArr">
<img class="show" :src="item.url" alt="">
<img class="close" src="images/close.png" @click="removeImg(index)">
</div>
</div>
<div class="imgts">支持上传通话记录截图等凭证</div>
</div>
<div class="alertButt">
<div class="cancel">取消</div>
<div class="submit">提交</div>
<div class="cancel" @click="gjStore.isShow = false">取消</div>
<div class="submit" @click="gjSubmit">提交</div>
</div>
</div>
</div>
......@@ -218,11 +221,11 @@
<div class="title">转为成单</div>
<div class="ali">
<div class="til">成单理由</div>
<textarea class="input" placeholder="请输入"></textarea>
<textarea v-model="cdStore.des" class="input" placeholder="请输入"></textarea>
</div>
<div class="alertButt">
<div class="cancel">取消</div>
<div class="submit">确认成单</div>
<div class="cancel" @click="cdStore.isShow=false">取消</div>
<div class="submit" @click="cdSubmit">确认成单</div>
</div>
<div class="botTs">提交后将进入审核流程,审核通过前可以撤回</div>
</div>
......@@ -233,7 +236,7 @@
<div class="ali">
<div class="til">请选择关闭原因</div>
<div class="choList">
<div class="choLi" v-for="item in gbStore.choArr" @click="item.isCho=!item.isCho">
<div class="choLi" v-for="(item,index) in gbStore.choArr" @click="gbReasonCho(index)">
<img v-if="!item.isCho" src="images/noCho.png" alt="">
<img v-else src="images/cho.png" alt="">
<div class="right">{{item.value}}</div>
......@@ -241,8 +244,8 @@
</div>
</div>
<div class="alertButt">
<div class="cancel">取消</div>
<div class="submit">确认关闭</div>
<div class="cancel" @click="gbStore.isShow = false">取消</div>
<div class="submit" @click="gbSubmit">确认关闭</div>
</div>
<div class="botTs">关闭后将无法再进行任何编辑操作</div>
</div>
......
......@@ -27,52 +27,38 @@ body {
padding-bottom: 1.2rem; /* 为底部按钮预留空间 */
}
/* 顶部标题栏 */
.header {
background-color: white;
border-bottom: 1px solid #f1f1f1;
position: sticky;
top: 0;
z-index: 100;
height: 0.88rem;
.loginOut{
display: flex;
align-items: center;
justify-content: center;
padding: 0 0.32rem;
border-radius: .08rem;
background: #EBEDF0;
width: 1.8rem;
height: .6rem;
color: #666;
font-size: .28rem;
margin: .64rem auto;
}
.loginOut img{
width: .32rem;
height: .32rem;
margin-right: .08rem;
}
.back-button {
position: absolute;
left: 0.32rem;
top: 50%;
transform: translateY(-50%);
width: 0.96rem;
height: 0.96rem;
background: none;
border: none;
cursor: pointer;
.topBut{
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 0.2rem;
height: 0.36rem;
}
.back-icon img {
width: 100%;
height: 100%;
object-fit: contain;
justify-content: flex-end;
margin-bottom: .2rem;
}
.header-title {
font-family: 'Source Han Sans CN', sans-serif;
font-weight: bold;
font-size: 0.36rem;
line-height: 0.36rem;
color: #333333;
.topBut div{
border-radius: 4px;
background: #0068EE;
color: #fff;
font-size: .26rem;
font-weight: 500;
padding: .2rem .4rem;
margin-right: .1rem;
}
/* 通用卡片样式 */
......@@ -190,11 +176,6 @@ body {
user-select: none;
}
.tag-button:hover {
border-color: #0068ee;
transform: translateY(-1px);
}
.tag-button.active {
background-color: #0068ee;
border-color: #0068ee;
......
......@@ -90,6 +90,7 @@ body {
width: 100%;
border: none;
display: block;
font-size: .29rem;
}
.alertCon .ali textarea::placeholder{
color: #999;
......@@ -106,6 +107,7 @@ body {
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.alertCon .ali .imgList .addImg img.add{
width: .56rem;
......
......@@ -53,6 +53,29 @@ div{
font-size: .32rem;
}
.tabList{
display: flex;
margin: .48rem;
color: #666;
font-size: .32rem;
background: #F1F4F9;
border-radius: .16rem;
height: .96rem;
align-items: center;
padding: .08rem;
}
.tabList div{
width: 33.33%;
text-align: center;
}
.tabList div.cho{
border-radius: .12rem;
background: #FFF;
color: #0068EE;
height: .8rem;
line-height: .8rem;
}
.logo {
width: .8rem;
height: .8rem;
......
......@@ -25,7 +25,7 @@ body {
position: relative;
}
.worker-con{
padding-bottom: 1.31rem; /* 为底部导航预留空间 */
padding-bottom: 1.31rem; /* 为底部导航预留空间 */
}
/* 搜索区域 */
......@@ -117,6 +117,7 @@ body {
.filter-tags {
display: flex;
padding: 0.32rem;
padding-bottom: 0;
}
.filter-tag {
......@@ -133,14 +134,33 @@ body {
color: #0068EE;
}
.loginOut{
display: flex;
align-items: center;
justify-content: center;
border-radius: .08rem;
background: #EBEDF0;
width: 1.8rem;
height: .6rem;
color: #666;
font-size: .28rem;
margin: .64rem auto;
}
.loginOut img{
width: .32rem;
height: .32rem;
margin-right: .08rem;
}
/* 商机列表 */
.business-list {
padding: 0 0.32rem;
height: calc(100vh - 3.8rem);
height: calc(100vh - 2.8rem);
overflow: auto;
margin-top: .32rem;
}
.worker-con .business-list{
height: calc(100vh - 5.2rem);
height: calc(100vh - 3.8rem);
}
.business-card {
......
......@@ -6,9 +6,19 @@ new Vue({
data: {
isAlone: true,
// 联系方式
contactPhone: '138****1234',
contactPhone: '',
// 商机类型
businessTypes: [],
recordingUrlArr: [], // 录音文件URL
// 选中地址文本
selectedAddressCode: '',
selectedAddressText: '',
detailAddress: '',
// 文字描述
textDescription: '',
// API相关
isSubmitting: false,
// 弹窗相关
modifyPhone:{
......@@ -17,57 +27,12 @@ new Vue({
phone: '',
},
// 商机类型
businessTypes: [
{
id: 1,
name: '融合套餐',
selected: false,
nodeId: '6:270'
},
{
id: 2,
name: '家庭安防',
selected: false,
nodeId: '6:272'
},
{
id: 3,
name: '智能家居',
selected: false,
nodeId: '6:274'
},
{
id: 4,
name: '企业专线',
selected: false,
nodeId: '6:276'
},
{
id: 5,
name: '云服务',
selected: false,
nodeId: '6:278'
},
{
id: 6,
name: '宽带升级',
selected: false,
nodeId: '6:613'
}
],
// 语音录制状态
isRecording: false,
recordingTimer: null,
recordingDuration: 0,
recordingUrlArr: [], // 录音文件URL
// 文字描述
textDescription: '',
// API相关
isSubmitting: false,
// 用户信息
userInfo: null,
......@@ -82,24 +47,18 @@ new Vue({
selectedDistrict: null,
selectedStreet: null
},
// 选中地址文本
selectedAddressText: '',
// 使用外部地址数据
addressData: addressData
},
addressData: addressData,
created() {
// 页面初始化时获取用户信息
timeWriteStr: '',
nIndex: '--',
},
mounted() {
this.getUserInfo()
this.isAlone = utils.getUrlParam('source')!='zhijian'
// 页面加载完成后的初始化
console.log('商机信息页面已加载');
this.queryLabel()
this.getUserInfo()
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', this.handleVisibilityChange);
......@@ -111,17 +70,59 @@ new Vue({
},
methods: {
goZj(){
location.href = 'result.html'
},
loginOut(){
localStorage.removeItem('userInfo')
localStorage.removeItem('tokenInfo')
location.replace('login.html?platform='+localStorage.getItem('platform'))
},
navigateToList(){
location.href = 'myBusi.html'
},
queryLabel(){
utils.httpRequest({
url: '/opportunity/tags',
data: {}
}).then(res=>{
if(res.code == 200){
res.data.forEach(item=>{
item.selected = false,
item.ifCho = true
})
this.businessTypes = res.data.concat([{
id: '3',
tagName: '测试3',
selected: false,
ifCho: true
},{
id: '4',
tagName: '测试4',
selected: false,
ifCho: true
},{
id: '5',
tagName: '测试5',
selected: false,
ifCho: true
}])
}
})
},
/**
* 获取用户信息
*/
getUserInfo() {
try {
const loginInfo = localStorage.getItem('appLoginInfo');
const loginInfo = localStorage.getItem('userInfo');
if (loginInfo) {
this.userInfo = JSON.parse(loginInfo);
// 如果有用户手机号,可以脱敏显示
if (this.userInfo && this.userInfo.phone) {
if (this.userInfo && this.userInfo.phone && !this.isAlone) {
this.contactPhone = this.maskPhone(this.userInfo.phone);
}
}
......@@ -145,44 +146,73 @@ new Vue({
*/
editContact() {
// 打开弹窗
this.newPhone = '';
this.modifyPhone.phone = '';
this.modifyPhone.isShow = true;
},
/**
* 关闭编辑弹窗
*/
closeEditModal() {
this.modifyPhone.newPhone = '';
this.modifyPhone.isShow = false
},
submitPhone(){
if(!this.modifyPhone.phone){
util.toast('请输入手机号')
utils.toast('请输入手机号')
return
}
if(!/^1[3-9]\d{9}$/.test(this.modifyPhone.phone)){
util.toast('手机号格式不正确')
utils.toast('手机号格式不正确')
return
}
this.contactPhone = this.modifyPhone.phone
this.modifyPhone.isShow = false
},
/**
* 选择商机类型
*/
selectBusinessType(typeId) {
const type = this.businessTypes.find(t => t.id === typeId);
if (type) {
type.selected = !type.selected;
const type = this.businessTypes.find(t => t.id === typeId)
if(!type.ifCho){
return
}
if (type) {
// 限制最多选择3个商机类型
const selectedCount = this.businessTypes.filter(t => t.selected).length;
if (selectedCount > 3) {
type.selected = false;
utils.toast('最多只能选择3个商机类型');
}else{
type.selected = !type.selected
let arr = this.businessTypes.filter(t => t.selected)
if(arr.length>0){
let idarr = arr.map(item=>{return item.id})
this.businessTypes.forEach(item=>{
if(idarr.includes(1) || idarr.includes(2)){
if(item.id!=1 && item.id!=2){
item.ifCho = false
}else{
item.ifCho = true
}
}else{
if(item.id==1 || item.id==2){
item.ifCho = false
}else{
item.ifCho = true
}
}
})
}else{
this.businessTypes.forEach(item=>{
item.ifCho = true
})
}
}
// 添加触觉反馈
......@@ -213,37 +243,68 @@ new Vue({
}
},
playAudio(index){
async playAudio(index){
let pa = this.recordingUrlArr[index]
let __this = this
let obj = this.$refs.audioObj
if(!pa.url){
pa.url = URL.createObjectURL(pa.blob)
}
if(obj.src == pa.url){
const isPlaying = !obj.paused && !obj.ended && obj.readyState > 2;
const audioURL = URL.createObjectURL(pa.blob);
__this.$refs.audioObj.src = audioURL
if(isPlaying){
obj.pause()
pa.isPlay = false
this.writeTime(1)
}else{
obj.play()
pa.isPlay = true
this.writeTime(2,pa,obj)
}
return
}
if(this.nIndex != '--'){
clearInterval(this.timeWriteStr)
this.recordingUrlArr[this.nIndex].time = obj.duration.toFixed(0)+'s'
this.recordingUrlArr[this.nIndex].isPlay = false
}
__this.$refs.audioObj.onloadeddata = async () => {
obj.src = pa.url
this.nIndex = index
obj.onloadeddata = async () => {
try {
await __this.$refs.audioObj.play()
await obj.play()
this.writeTime(2,pa,obj)
pa.isPlay = true
console.log('音频自动播放');
} catch (error) {
console.log('自动播放被阻止,需要用户交互');
}
};
// const audioURL = URL.createObjectURL(pa.blob);
// const audio = document.createElement('audio');
// audio.controls = true;
// audio.src = audioURL;
// document.body.appendChild(audio);
}
let __this = this
obj.addEventListener('ended', () => {
clearInterval(__this.timeWriteStr)
pa.time = obj.duration.toFixed(0)+'s'
pa.isPlay = false
__this.nIndex = '--'
});
},
writeTime(type,pa,obj){
if(type == 1){
clearInterval(this.timeWriteStr)
}else{
this.timeWriteStr = setInterval(()=>{
pa.time = (obj.duration - obj.currentTime).toFixed(0) + 's'
},1000)
}
},
removeAudio(index){
this.$refs.audioObj.pause()
this.recordingUrlArr.splice(index , 1)
},
getTimes(num){
const minutes = Math.floor(this.duration / 60);
const seconds = this.duration % 60;
return
},
/**
* 开始录音
......@@ -262,19 +323,19 @@ new Vue({
sampleRate: 44100
}
});
const mimeType = this.getSupportedMimeType()
const mediaRecorder = new MediaRecorder(__this.mediaStream,{
mimeType: 'audio/webm;codecs=opus'
mimeType: mimeType
});
const chunks = [];
mediaRecorder.ondataavailable = e => chunks.push(e.data);
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'audio/webm;codecs=opus' });
const blob = new Blob(chunks, { type: mimeType });
__this.recordingUrlArr.push({
blob: blob,
isPlay: false,
time: __this.recordingDuration
time: __this.recordingDuration+"s"
})
};
......@@ -294,6 +355,25 @@ new Vue({
}
},
getSupportedMimeType(){
const types = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/ogg;codecs=opus',
'audio/ogg',
'audio/wav',
'audio/mp4;' // 某些浏览器支持
];
for (let type of types) {
if (MediaRecorder.isTypeSupported(type)) {
console.log('使用格式:', type);
return type;
}
}
return 'audio/webm'; // 默认回退
},
/**
* 设置音频处理
*/
......@@ -302,7 +382,7 @@ new Vue({
// 暂时使用简单的计时器
this.recordingTimer = setInterval(() => {
this.recordingDuration++;
if (this.recordingDuration >= 60*60) { // 限制最长录音一小时
if (this.recordingDuration >=60) { // 限制最长录音一分钟
this.stopRecording();
utils.toast('录音时间已到最长限制');
}
......@@ -371,71 +451,75 @@ new Vue({
},
/**
* 获取选中的商机类型名称
*/
getSelectedBusinessTypes() {
return this.businessTypes
.filter(type => type.selected)
.map(type => type.name);
},
/**
* 提交商机
*/
async submitBusiness() {
// 表单验证
if (!this.validateForm()) {
if (this.isAlone && !this.contactPhone) {
utils.toast('请输入手机号');
return;
}
if (!/^1[3-9]\d{9}$/.test(this.contactPhone)) {
utils.toast('请输入正确的手机号');
return;
}
if (!this.selectedAddressText && this.isAlone) {
utils.toast('请选择用户地址');
return;
}
if (!this.detailAddress && this.isAlone) {
utils.toast('请输入详细地址');
return;
}
// 检查是否选择了商机类型
const selectedTypes = this.businessTypes.filter(t => t.selected);
if (selectedTypes.length === 0) {
utils.toast('请选择商机类型');
return false;
}
if (this.isSubmitting) {
utils.toast('正在提交,请稍候...');
return;
}
this.isSubmitting = true;
utils.toast('正在提交商机信息...');
this.isSubmitting = true
try {
// 准备提交数据
const businessData = {
// 商机类型
businessTypes: this.getSelectedBusinessTypes(),
// 描述信息
textDescription: this.textDescription.trim(),
voiceDuration: this.recordingDuration,
// 联系方式
contactPhone: this.contactPhone,
// 用户信息
userId: this.userInfo ? this.userInfo.userId : null,
// 提交时间
submitTime: new Date().toISOString(),
// 设备信息
deviceInfo: {
userAgent: navigator.userAgent,
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
platform: navigator.platform,
language: navigator.language
}
};
const fd = new FormData();
fd.append('customerPhone',this.contactPhone)
fd.append('customerAddress',this.selectedAddressCode?(this.selectedAddressCode+":"+this.selectedAddressText+this.detailAddress):'')
const tagArr = selectedTypes.map(item=>{return item.id})
fd.append('opportunityType',(tagArr.includes(1)||tagArr.includes(2))?'2':'1')
fd.append('tagIds',tagArr.join(','))
fd.append('textDescription',this.textDescription)
fd.append('audioType',this.getSupportedMimeType())
if(this.recordingUrlArr.length > 0){
this.recordingUrlArr.forEach(item=>{
fd.append('audioFiles',item.blob)
})
}
console.log(fd)
// 调用API提交
const result = await this.submitBusinessAPI(businessData);
const result = await utils.httpRequest({
url: '/opportunity/create',
data: fd
})
if (result.success) {
utils.toast('商机提交成功!');
// 添加触觉反馈
this.addHapticFeedback();
if (result.code == 200) {
utils.toast('商机提交成功!')
// 延迟后重置表单或跳转
setTimeout(() => {
this.handleSubmitSuccess();
}, 2000);
location.href = 'myBusi.html'
}, 1000);
} else {
utils.toast(result.message || '提交失败,请重试');
}
......@@ -685,20 +769,26 @@ new Vue({
*/
confirmAddressSelection() {
const parts = [];
const codes = [];
if (this.addressSelector.selectedProvince) {
codes.push(this.addressSelector.selectedProvince.code);
parts.push(this.addressSelector.selectedProvince.name);
}
if (this.addressSelector.selectedCity) {
codes.push(this.addressSelector.selectedCity.code);
parts.push(this.addressSelector.selectedCity.name);
}
if (this.addressSelector.selectedDistrict) {
codes.push(this.addressSelector.selectedDistrict.code);
parts.push(this.addressSelector.selectedDistrict.name);
}
if (this.addressSelector.selectedStreet) {
codes.push(this.addressSelector.selectedStreet.code);
parts.push(this.addressSelector.selectedStreet.name);
}
this.selectedAddressCode = codes.join('|');
this.selectedAddressText = parts.join('');
this.closeAddressSelector();
this.addHapticFeedback();
......@@ -737,29 +827,6 @@ new Vue({
// 计算属性
computed: {
/**
* 已选择的商机类型数量
*/
selectedTypesCount() {
return this.businessTypes.filter(type => type.selected).length;
},
/**
* 是否可以提交
*/
canSubmit() {
return this.selectedTypesCount > 0 &&
(this.textDescription.trim() || this.recordingDuration > 0) &&
!this.isSubmitting;
},
/**
* 格式化的录音时长显示
*/
formattedRecordingDuration() {
return this.formatRecordingDuration(this.recordingDuration);
},
/**
* 地址选择标签页
*/
addressTabs() {
......
......@@ -5,39 +5,12 @@ new Vue({
el: '#app',
data: {
isWorker: false,
// 商机详情数据 - 按照设计图中的真实数据
businessDetail: {
businessId: 'OP20250928002', // 商机ID
status: '跟进中', // 状态
statusDescription: '营销人员 [张三] 将状态变更为跟进中', // 状态描述
customerAddress: '江苏省淮安市淮安区淮海南路456号', // 客户地址
customerAccount: '13477895643', // 客户账号
customerPhone: '155****3344', // 客户电话
marketerPhone: '131****3564', // 营销人员电话
marketingTypes: ['宽带升级', '家庭安防'], // 营销类型
submitTime: '2025-09-26 11:20:00', // 提交时间
voiceRecords: [
{
duration: '00:04',
converted: false,
isPlaying: false
},
{
duration: '00:04',
converted: false,
isPlaying: false
}
],
textDescription: '客户家里目前使用的是50兆的宽带,感觉网速有点慢,想要升级到100兆或者200兆。客户还问了一下家庭套餐的情况,想了解一下手机和宽带一起办理有没有优惠。', // 文字描述
adminNotes: '客户比较倾向于性价比高的方案,建议重点推荐融合套餐', // 管理员备注
followUpStatus: '跟进中', // 跟进状态
followUpTime: '2025-09-29 11:10', // 跟进时间
followUpDescription: '营销人员 [张三] 添加跟进:"已上门与客户沟通,客户确认办理融合套餐,待签单。', // 跟进描述
createTime: '2025-09-29 09:30', // 创建时间
createDescription: '张师傅 提交了此商机' // 创建描述
},
isWorker: true,
platform: '',
detailId: '',
businessDetail: {},
followList: [],
followNewInfo:{},
// 页面状态
isLoading: false,
......@@ -49,9 +22,13 @@ new Vue({
gjStore: {
isShow: false,
des: '',
imgArr: [],
isLoading: false
},
cdStore: {
des: '',
isShow: false
},
gbStore: {
......@@ -76,12 +53,19 @@ new Vue({
isCho: false,
value: '其他原因'
}]
}
},
timeWriteStr: '',
nIndex: '--'
},
created() {
// 初始化页面
this.initializeApp();
this.platform = localStorage.getItem('platform')
this.isWorker = localStorage.getItem('platform')=='gw'
this.detailId = utils.getUrlParam('id')
this.queryDetail()
this.queryFollow()
},
mounted() {
......@@ -95,152 +79,276 @@ new Vue({
},
methods: {
fileChange(e){
if(this.gjStore.isLoading){
getAddressShow(value){
if(!value){
return '--'
}
return value.split(":")[1]
},
addBusiProcess(){
if(!this.ifCanGj){
return
}
let file = e.target.files[0]
console.log(file)
if(file.type.includes('image/') <= 0){
util.toast('请上传图片')
this.gjStore.des = ''
this.gjStore.imgArr = []
this.gjStore.isShow = true
},
async gjSubmit(){
let d = this.gjStore
if(!d.des){
utils.toast('请输入跟进内容')
return
}
// if(d.imgArr.length <= 0){
// utils.toast('请上传图片')
// return
// }
const fd = new FormData();
fd.append('followContent',d.des)
fd.append('opportunityId',this.detailId)
d.imgArr.forEach(item=>{
fd.append('images',item.file)
})
this.uploadImg(file)
},
uploadImg(file){
let fileFormData = new FormData()
fileFormData.append('file', file)
this.gjStore.isLoading = true
return
util.httpRequest({
url: '/api/comfyui/uploadImage',
data: fileFormData
}).then(res=>{
this.loading = false
if(res.code =='200'){
this['img'+type] = res.data.name
}else{
util.toast(res.msg)
}
// 调用API提交
const result = await utils.httpRequest({
url: '/opportunity/follow',
data: fd
})
},
/**
* 初始化应用
*/
initializeApp() {
// 获取URL参数
this.getUrlParams();
if (result.code == 200) {
utils.toast('跟进提交成功!');
this.gjStore.isShow = false
this.queryFollow()
} else {
utils.toast(result.message || '提交失败,请重试');
}
},
completeBusi(){
if(!this.ifCanCd){
return
}
// 获取用户信息
this.getUserInfo();
this.cdStore.des = ''
// 加载商机详情
this.loadBusinessDetail();
this.cdStore.isShow = true
},
async cdSubmit(){
let d = this.cdStore
/**
* 获取URL参数
*/
getUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
this.businessDetail.businessId = urlParams.get('id') || this.businessDetail.businessId;
},
if(!d.des){
utils.toast('请输入成单理由')
return
}
/**
* 获取用户信息
*/
getUserInfo() {
try {
const loginInfo = localStorage.getItem('appLoginInfo');
if (loginInfo) {
this.userInfo = JSON.parse(loginInfo);
// 调用API提交
const result = await utils.httpRequest({
url: '/opportunity/deal',
data: {
opportunityId: this.detailId,
reason: d.des
}
} catch (error) {
console.error('获取用户信息失败:', error);
})
if (result.code == 200) {
utils.toast('成单成功!');
this.cdStore.isShow = false
this.queryFollow()
} else {
utils.toast(result.message || '提交失败,请重试');
}
},
closeBusi(){
if(!this.ifCanGb){
return
}
/**
* 加载商机详情
*/
async loadBusinessDetail() {
this.isLoading = true;
this.gbStore.choArr.forEach(item=>{
item.isCho = false
})
try {
// 模拟API调用 - 实际项目中替换为真实API
await this.simulateAPICall();
this.gbStore.isShow = true
},
gbReasonCho(index){
this.gbStore.choArr.forEach(item=>{
item.isCho = false
})
this.gbStore.choArr[index].isCho = true
},
async gbSubmit(){
let d = this.gbStore
// 实际项目中的API调用
// const result = await this.getBusinessDetailAPI(this.businessDetail.businessId);
let arr = d.choArr.filter(item=>{return item.isCho})
if(arr.length <= 0){
utils.toast('请选择原因')
return
}
} catch (error) {
console.error('加载商机详情失败:', error);
utils.toast('加载失败,请重试');
} finally {
this.isLoading = false;
// 调用API提交
const result = await utils.httpRequest({
url: '/opportunity/close',
data: {
opportunityId: this.detailId,
reason: arr[0].value
}
})
if (result.code == 200) {
utils.toast('关闭成功!');
this.gbStore.isShow = false
this.queryDetail()
} else {
utils.toast(result.message || '提交失败,请重试');
}
},
getStatusName(value){
if(value == '1'){
return '待跟进'
}else if(value == '2'){
return '跟进中'
}else if(value == '3'){
return '成单待审核'
}else if(value == '4'){
return '已成单'
}else if(value == '5'){
return '已关闭'
}
/**
* 模拟API调用
*/
simulateAPICall() {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟加载效果,实际使用上面定义的静态数据
resolve();
}, 800);
});
return '--'
},
queryFollow(){
utils.httpRequest({
url: '/opportunity/follow/list',
data: {
opportunityId: this.detailId,
pageNum: 1,
pageSize: 1000
}
}).then(res=>{
if(res.code == 200){
this.followList = res.data.records
/**
* 获取语音记录节点ID
*/
getVoiceNodeId(index) {
const baseId = 2942377;
return `294:${baseId + index}`;
let pa = res.data.records[0]
this.followNewInfo = {
statusName: this.getStatusName(pa.status),
des: pa.followPersonName+':'+pa.followContent
}
}
})
},
queryDetail(){
utils.httpRequest({
url: '/opportunity/detail',
data: {
opportunityId: this.detailId
}
}).then(res=>{
if(res.code == 200){
if(res.data.voiceDescriptionUrl){
let arr = []
res.data.voiceDescriptionUrl.split(',').forEach(item=>{
arr.push({
url: item,
text: '',
isPlay: false,
time: ''
})
})
res.data.voiceRecords = arr
}
/**
* 获取波形条高度
*/
getWaveformHeight(n) {
// 为31个波形条生成不同的高度,模拟真实波形显示
const heights = [
0.02, 0.06, 0.10, 0.14, 0.18, 0.22, 0.26, 0.30, 0.34, 0.38,
0.42, 0.38, 0.34, 0.30, 0.26, 0.22, 0.18, 0.14, 0.10, 0.06,
0.02, 0.06, 0.10, 0.14, 0.18, 0.22, 0.26, 0.30, 0.34, 0.38
];
return `${heights[n - 1] || 0.06}rem`;
this.businessDetail = res.data
}
})
},
async playAudio(index){
let pa = this.businessDetail.voiceRecords[index]
let obj = this.$refs.audioObj
/**
* 返回上一页
*/
goBack() {
if (window.history.length > 1) {
window.history.back();
} else {
// 如果没有历史记录,返回到商机列表
window.location.href = 'busiList.html';
if(obj.src == pa.url){
const isPlaying = !obj.paused && !obj.ended && obj.readyState > 2;
if(isPlaying){
obj.pause()
pa.isPlay = false
this.writeTime(1)
}else{
obj.play()
pa.isPlay = true
this.writeTime(2,pa,obj)
}
return
}
if(this.nIndex != '--'){
clearInterval(this.timeWriteStr)
this.businessDetail.voiceRecords[this.nIndex].time = obj.duration.toFixed(0)+'s'
this.businessDetail.voiceRecords[this.nIndex].isPlay = false
}
obj.src = pa.url
this.nIndex = index
obj.onloadeddata = async () => {
try {
await obj.play()
this.writeTime(2,pa,obj)
pa.isPlay = true
console.log('音频自动播放');
console.log(this.businessDetail.voiceRecords)
} catch (error) {
console.log('自动播放被阻止,需要用户交互');
}
}
let __this = this
obj.addEventListener('ended', () => {
clearInterval(__this.timeWriteStr)
pa.time = obj.duration.toFixed(0)+'s'
pa.isPlay = false
__this.nIndex = '--'
});
},
writeTime(type,pa,obj){
if(type == 1){
clearInterval(this.timeWriteStr)
}else{
clearInterval(this.timeWriteStr)
this.timeWriteStr = setInterval(()=>{
pa.time = (obj.duration - obj.currentTime).toFixed(0) + 's'
},1000)
}
},
fileChange(e){
if(this.gjStore.isLoading){
return
}
let file = e.target.files[0]
console.log(file)
if(file.type.includes('image/') <= 0){
util.toast('请上传图片')
return
}
this.gjStore.imgArr.push({
file: file,
url: URL.createObjectURL(file)
})
},
/**
* 复制客户电话
*/
copyPhone() {
const phone = this.businessDetail.customerPhone;
// 从脱敏号码还原(实际项目中应该从API获取完整号码)
const fullPhone = '15555553344';
const fullPhone = this.businessDetail.customerPhone;
if (navigator.clipboard) {
navigator.clipboard.writeText(fullPhone).then(() => {
utils.toast('手机号已复制');
utils.toast('客户电话');
this.addHapticFeedback();
}).catch(() => {
this.fallbackCopyToClipboard(fullPhone);
......@@ -254,9 +362,7 @@ new Vue({
* 复制营销人员电话
*/
copyMarketerPhone() {
const phone = this.businessDetail.marketerPhone;
// 从脱敏号码还原
const fullPhone = '13111113564';
const fullPhone = this.businessDetail.marketingStaffPhone;
if (navigator.clipboard) {
navigator.clipboard.writeText(fullPhone).then(() => {
......@@ -303,26 +409,23 @@ new Vue({
}
},
/**
* 致电营销人员
*/
callMarketer() {
const phone = this.businessDetail.marketerPhone;
const fullPhone = '13111113564'; // 实际号码
if (typeof device !== 'undefined' && device.platform === 'iOS') {
window.location.href = `tel:${fullPhone}`;
} else {
window.open(`tel:${fullPhone}`, '_self');
}
switchTime(value){
return utils.detailTime(new Date(value).getTime())
},
/**
* 联系客户
* 打电话
*/
callCustomer() {
const phone = this.businessDetail.customerPhone;
const fullPhone = '15555553344'; // 实际号码
takeCall(type){
//客户电话
let fullPhone = this.businessDetail.customerPhone
if(type == 'yx'){
//营销人员电话
fullPhone = this.businessDetail.marketingStaffPhone
}else if(type == 'zw'){
//装维师傅电话
fullPhone = this.businessDetail.maintenanceStaffPhone
}
if (typeof device !== 'undefined' && device.platform === 'iOS') {
window.location.href = `tel:${fullPhone}`;
......@@ -474,6 +577,24 @@ new Vue({
}
},
audioToText(index){
let pa = this.businessDetail.voiceRecords[index]
utils.httpRequest({
url: '/audio-to-text',
middle: 'common',
data: {
audioUrl: pa.url
}
}).then(res=>{
if(res.code == 200){
pa.text = res.data.textContent
}else{
utils.toast(res.message)
}
})
},
/**
* 清理资源
*/
......@@ -493,6 +614,15 @@ new Vue({
// 计算属性
computed: {
ifCanGj(){
return this.businessDetail.status==1||this.businessDetail.status==2
},
ifCanCd(){
return this.businessDetail.status==1||this.businessDetail.status==2
},
ifCanGb(){
return this.businessDetail.status==1||this.businessDetail.status==2||this.businessDetail.status==3
},
/**
* 是否有跟进状态
*/
......@@ -524,31 +654,6 @@ new Vue({
// 监听器
watch: {
/**
* 监听语音播放状态变化
*/
'businessDetail.voiceRecords': {
handler(newRecords) {
const isAnyPlaying = newRecords.some(record => record.isPlaying);
if (isAnyPlaying) {
document.title = '播放语音中...';
} else {
document.title = '商机详情';
}
},
deep: true
},
/**
* 监听页面可见性变化
*/
isAnyPlaying(newVal) {
if (document.hidden && newVal) {
// 页面隐藏时暂停所有播放
this.businessDetail.voiceRecords.forEach(record => {
record.isPlaying = false;
});
}
}
}
});
\ No newline at end of file
......@@ -3,16 +3,33 @@ const util = new window.publicMethod() ;
new Vue({
el: '#app',
data: {
platform: 'yx',
loginForm: {
employeeId: '',
phoneNumber: '',
verifyCode: ''
employeeId: '23123456',
phoneNumber: '13718590607',
verifyCode: '123456'
},
countdown: 0,
countdownTimer: null,
clickFlag: false
},
methods: {
tabChang(type){
this.platform = type
localStorage.setItem('platform',type)
if(type == 'gw'){
this.loginForm.employeeId = '1235456'
this.loginForm.phoneNumber = '13718596969'
}else if(type == 'yx'){
this.loginForm.employeeId = '23123456'
this.loginForm.phoneNumber = '13718590607'
}else{
this.loginForm.employeeId = '15862709858'
this.loginForm.phoneNumber = '15862709858'
}
},
getVerifyCode() {
let pa = this.loginForm
......@@ -37,10 +54,9 @@ new Vue({
this.clickFlag = true
util.httpRequest({
url: '/mobile/sendSms',
middleUrl: '/zhijian-trial/api',
url: '/send-sms',
data: {
campaignId: pa.employeeId,
personnelCode: pa.employeeId,
phone: pa.phoneNumber,
},
}).then(res=>{
......@@ -59,7 +75,7 @@ new Vue({
}, 1000);
}else{
this.clickFlag = false
util.toast(res.msg || '获取失败')
util.toast(res.message || '获取失败')
}
}).catch(()=>{
this.clickFlag = false
......@@ -98,24 +114,30 @@ new Vue({
}
util.httpRequest({
url: '/mobile/login',
middleUrl: '/zhijian-trial/api',
url: '/phone-login',
data: {
campaignId: pa.employeeId,
personnelCode: pa.employeeId,
phone: pa.phoneNumber,
smsCode: pa.verifyCode
},
}).then(res=>{
if(res.code == 200){
localStorage.setItem('userInfo',JSON.stringify(res.data.personnel))
localStorage.setItem('tokenInfo',JSON.stringify(res.data.tokenInfo))
if(this.platform == 'gw'){
window.location.href = 'addBusi.html'
}else{
window.location.href = 'myBusi.html'
}
}else{
util.toast(res.msg || '登录失败')
util.toast(res.message || '登录失败')
}
})
}
},
created(){
},
beforeDestroy() {
// 清除定时器
......
// 商机信息页面 Vue 应用
const utils = new publicMethod()
new Vue({
el: '#app',
data: {
isWorker: true,
detail: {
records: []
},
searchKeyword: '',
activeTab: 'completed',
activeTab: 'all',
tagId: 'all',
filterTags: [
{ id: 1, name: '全部', selected: true, nodeId: '355:535' },
{ id: 2, name: '成单待审核', selected: false, nodeId: '355:537' },
{ id: 3, name: '已成单', selected: false, nodeId: '355:540' },
{ id: 4, name: '已关闭', selected: false, nodeId: '355:543' }
{ id: 'all', name: '全部', selected: true, nodeId: '355:535' },
{ id: '3', name: '成单待审核', selected: false, nodeId: '355:537' },
{ id: '4', name: '已成单', selected: false, nodeId: '355:540' },
{ id: '5', name: '已关闭', selected: false, nodeId: '355:543' }
],
businessList: [
{
id: 1,
orderNumber: 'OP20250928002',
status: 'follow-up',
statusText: '跟进中',
statusClass: 'follow-up',
nodeId: '355:430',
address: '江苏省淮安市淮安区淮海南路456号',
contactPhone: '13477895643',
tags: [
{ id: 1, name: '宽带升级', nodeId: '355:453' },
{ id: 2, name: '家庭安防', nodeId: '355:455' }
],
processor: '李四',
submitTime: '2025-09-28 16:45',
updateTime: '2025-09-29 09:30'
},
{
id: 2,
orderNumber: 'OP20250928003',
status: 'completed',
statusText: '已完结',
statusClass: 'completed',
nodeId: '355:465',
address: '江苏省淮安市淮安区淮海南路456号',
contactPhone: '13477895643',
tags: [
{ id: 1, name: '宽带升级', nodeId: '355:488' },
{ id: 2, name: '家庭安防', nodeId: '355:490' }
],
processor: '张三',
submitTime: '2025-09-28 16:45',
updateTime: '2025-09-29 09:30'
},
{
id: 3,
orderNumber: 'OP20250927001',
status: 'pending',
statusText: '待跟进',
statusClass: 'pending',
nodeId: '355:491',
address: '江苏省淮安市清江浦区健康东路123号',
contactPhone: '13912345678',
tags: [
{ id: 3, name: '企业宽带', nodeId: '355:492' }
],
processor: '王五',
submitTime: '2025-09-27 10:20',
updateTime: '2025-09-28 14:15'
},
{
id: 4,
orderNumber: 'OP20250926005',
status: 'closed',
statusText: '已关闭',
statusClass: 'closed',
nodeId: '355:493',
address: '江苏省淮安市经济技术开发区枚皋中路789号',
contactPhone: '15898765432',
tags: [
{ id: 4, name: '云服务', nodeId: '355:494' },
{ id: 5, name: 'IDC业务', nodeId: '355:495' }
],
processor: '赵六',
submitTime: '2025-09-26 15:30',
updateTime: '2025-09-27 11:45'
}
]
businessList: []
},
computed: {
// 统计各状态数量
totalCount() {
return this.businessList.length;
},
pendingCount() {
return this.businessList.filter(item => item.status === 'pending').length;
},
followUpCount() {
return this.businessList.filter(item => item.status === 'follow-up').length;
},
completedCount() {
return this.businessList.filter(item => item.status === 'completed').length;
},
// 筛选后的商机列表
filteredBusinessList() {
let list = this.businessList;
// 按标签页筛选
if (this.activeTab !== 'all') {
list = list.filter(item => item.status === this.activeTab);
},
methods: {
addressShow(value){
if(!value || value==':'){
return '--'
}
// 按筛选标签筛选
const selectedTag = this.filterTags.find(tag => tag.selected);
if (selectedTag && selectedTag.id !== 1) {
// 这里可以根据实际业务逻辑进行筛选
switch(selectedTag.id) {
case 2: // 成单待审核
list = list.filter(item => item.status === 'completed');
break;
case 3: // 已成单
list = list.filter(item => item.status === 'completed');
break;
case 4: // 已关闭
list = list.filter(item => item.status === 'closed');
break;
return value.split(":")[1]
},
switchTime(value){
return utils.detailTime(new Date(value).getTime(),true)
},
queryBusiList(){
let status
if(this.activeTab == 'all'){
status = ''
}else{
if(this.activeTab == '3,4,5'){
if(this.tagId == 'all'){
status = '3,4,5'
}else{
status = this.tagId
}
}else{
status = this.activeTab
}
}
// 按搜索关键词筛选
if (this.searchKeyword) {
const keyword = this.searchKeyword.toLowerCase();
list = list.filter(item =>
item.orderNumber.toLowerCase().includes(keyword) ||
item.address.toLowerCase().includes(keyword) ||
item.contactPhone.includes(keyword) ||
item.processor.toLowerCase().includes(keyword)
);
}
utils.httpRequest({
url: '/opportunity/list',
data: {
customerPhone: this.searchKeyword,
status: status,
pageNum: 1,
pageSize: 1000
}
}).then(res=>{
if(res.code == 200){
this.detail = res.data
}
})
},
return list;
}
},
methods: {
// 切换标签页
switchTab(tab) {
this.activeTab = tab;
this.updateActiveIndicator(tab);
this.activeTab = tab
this.updateActiveIndicator(tab)
if(tab == '3,4,5'){
this.tagId = 'all'
}
this.queryBusiList()
},
// 更新活跃指示器位置
......@@ -151,9 +84,9 @@ new Vue({
let targetIndex = 0;
switch(tab) {
case 'all': targetIndex = 0; break;
case 'pending': targetIndex = 1; break;
case 'follow-up': targetIndex = 2; break;
case 'completed': targetIndex = 3; break;
case '1': targetIndex = 1; break;
case '2': targetIndex = 2; break;
case '3,4,5': targetIndex = 3; break;
}
if (indicator && tabTexts[targetIndex]) {
......@@ -168,25 +101,21 @@ new Vue({
// 选择筛选标签
selectFilterTag(tagId) {
this.filterTags.forEach(tag => {
tag.selected = tag.id === tagId;
});
this.tagId = tagId
this.queryBusiList()
},
// 查看商机详情
viewBusinessDetail(business) {
console.log('查看商机详情:', business);
// 实际项目中这里应该跳转到详情页面
// window.location.href = `busiDetail.html?id=${business.id}`;
// 或者显示详情弹窗
alert(`查看商机详情: ${business.orderNumber}`);
window.location.href = `busiDetail.html?id=${business.id}`;
},
// 跳转到收集商机页面
navigateToCollect() {
console.log('跳转到收集商机页面');
window.location.href = 'addbusi.html';
window.location.href = 'addBusi.html';
},
// 搜索功能
......@@ -205,89 +134,24 @@ new Vue({
// 执行搜索
performSearch() {
console.log('执行搜索:', this.searchKeyword);
// 搜索逻辑已在computed中实现
},
// 格式化时间
formatTime(timeString) {
if (!timeString) return '';
return timeString;
},
// 获取商机状态样式类
getStatusClass(status) {
const statusMap = {
'pending': 'pending',
'follow-up': 'follow-up',
'completed': 'completed',
'closed': 'closed'
};
return statusMap[status] || 'pending';
},
// 获取商机状态文本
getStatusText(status) {
const statusMap = {
'pending': '待跟进',
'follow-up': '跟进中',
'completed': '已完结',
'closed': '已关闭'
};
return statusMap[status] || '待跟进';
},
// 初始化数据
initializeData() {
console.log('初始化数据');
// 延迟更新指示器位置,确保DOM已渲染
this.$nextTick(() => {
this.updateActiveIndicator(this.activeTab);
});
},
// 处理页面可见性变化
handleVisibilityChange() {
if (document.visibilityState === 'visible') {
console.log('页面重新可见,刷新数据');
this.refreshData();
this.queryBusiList()
},
getStatusCalss(status){
if(status == '1'){
return 'pending'
}else if(status == '2'){
return 'follow-up'
}else if(status == '5'){
return 'closed'
}else{
return 'completed'
}
},
loginOut(){
localStorage.removeItem('userInfo')
localStorage.removeItem('tokenInfo')
// 处理网络连接恢复
handleOnline() {
console.log('网络连接恢复');
this.refreshData();
},
// 处理网络断开
handleOffline() {
console.log('网络连接断开');
this.showNetworkError();
},
// 刷新数据
refreshData() {
console.log('刷新商机列表数据');
// 实际项目中这里应该重新调用API获取最新数据
},
// 显示网络错误提示
showNetworkError() {
console.error('网络连接断开,请检查网络设置');
},
// 显示错误信息
showError(message) {
console.error(message);
alert(message);
},
// 显示成功信息
showSuccess(message) {
console.log(message);
alert(message);
location.replace('login.html?platform='+localStorage.getItem('platform'))
}
},
......@@ -304,22 +168,14 @@ new Vue({
// 生命周期钩子
mounted() {
console.log('我的商机页面已加载');
this.isWorker = localStorage.getItem('platform')=='gw'
// 初始化数据
this.initializeData();
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', this.handleVisibilityChange);
// 添加网络状态监听
window.addEventListener('online', this.handleOnline);
window.addEventListener('offline', this.handleOffline);
// 监听窗口大小变化,更新指示器位置
window.addEventListener('resize', () => {
// 延迟更新指示器位置,确保DOM已渲染
this.$nextTick(() => {
this.updateActiveIndicator(this.activeTab);
});
this.queryBusiList()
},
beforeDestroy() {
......@@ -327,11 +183,5 @@ new Vue({
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
// 移除事件监听
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
window.removeEventListener('online', this.handleOnline);
window.removeEventListener('offline', this.handleOffline);
window.removeEventListener('resize', this.updateActiveIndicator);
}
});
\ No newline at end of file
......@@ -16,6 +16,9 @@
}
},
methods: {
goAdd(){
location.href = 'addBusi.html?source=zhijian'
},
goBack(){
history.go(-1)
}
......
......@@ -72,8 +72,23 @@
*
*/
httpRequest:function(param){
let middle = param.middleUrl || '/zhijian-trial/ha'
let pa = localStorage.getItem('appLoginInfo')
let middle = '/compass/api/'
if(param.middle){
middle += param.middle
}else{
if(localStorage.getItem('platform') == 'gw'){
middle += 'personnel'
}
if(localStorage.getItem('platform') == 'yx'){
middle += 'marketing'
}
if(localStorage.getItem('platform') == 'gov'){
middle += 'gov-marketing'
}
}
let pa = localStorage.getItem('tokenInfo')
if(pa){
pa = JSON.parse(pa)
}else{
......@@ -88,7 +103,7 @@
data:param.data,
headers: {
'Content-Type': 'application/json',
'x-access-token': pa.tokenValue
'Authorization': pa.token||''
}
}).then((res) => {
resolve(res.data)
......@@ -97,6 +112,31 @@
})
})
},
detailTime: function(num,flag) {
if (num == null) {
return '';
} else {
num = Number(num);
let d = new Date(num);
let year = d.getFullYear();
let month = d.getMonth() + 1;
let date = d.getDate();
let hour = d.getHours();
let minute = d.getMinutes();
let second = d.getSeconds();
month < 10 ? month = '0' + month : month;
date < 10 ? date = '0' + date : date;
hour < 10 ? hour = '0' + hour : hour;
minute < 10 ? minute = '0' + minute : minute;
second < 10 ? second = '0' + second : second;
if(flag){
return year + "-" + month + "-" + date + " " + hour + ":" + minute
}
return year + "-" + month + "-" + date + " " + hour + ":" + minute + ":" + second;
}
},
}
window.publicMethod = utils
})()
\ No newline at end of file
......@@ -20,6 +20,12 @@
</div>
</div>
<div class="tabList">
<div :class="{cho: platform=='yx'}" @click="tabChang('yx')">C端支撑</div>
<div :class="{cho: platform=='gov'}" @click="tabChang('gov')">政企支撑</div>
<div :class="{cho: platform=='gw'}" @click="tabChang('gw')">装维师傅</div>
</div>
<div class="login-form">
<div class="input-group">
<img src="images/1.png" alt="工号图标" class="input-icon">
......@@ -58,7 +64,7 @@
<div @click="login" class="login-btn">登录</div>
<div class="loginTs">登录即表示您同意<span>《用户协议》</span><span>《隐私协议》</span></div>
<!-- <div class="loginTs">登录即表示您同意<span>《用户协议》</span>和<span>《隐私协议》</span></div> -->
</div>
</div>
</div>
......@@ -66,6 +72,6 @@
<script src="js/axios.min.js"></script>
<script src="js/vue.min.js"></script>
<script src="js/util.js"></script>
<script src="js/login.js?444"></script>
<script src="js/login.js?441234"></script>
</body>
</html>
\ No newline at end of file
......@@ -18,25 +18,25 @@
:class="{ active: activeTab === 'all' }"
@click="switchTab('all')"
data-node-id="355:516"
>全部商机{{ totalCount }}</div>
>全部商机{{ detail.totalCount }}</div>
<div
class="tab-text"
:class="{ active: activeTab === 'pending' }"
@click="switchTab('pending')"
:class="{ active: activeTab === '1' }"
@click="switchTab('1')"
data-node-id="355:517"
>待跟进{{ pendingCount }}</div>
>待跟进{{ detail.pendingCount }}</div>
<div
class="tab-text"
:class="{ active: activeTab === 'follow-up' }"
@click="switchTab('follow-up')"
:class="{ active: activeTab === '2' }"
@click="switchTab('2')"
data-node-id="355:518"
>跟进中{{ followUpCount }}</div>
>跟进中{{ detail.followingCount }}</div>
<div
class="tab-text"
:class="{ active: activeTab === 'completed' }"
@click="switchTab('completed')"
:class="{ active: activeTab === '3,4,5' }"
@click="switchTab('3,4,5')"
data-node-id="355:519"
>已完结{{ completedCount }}</div>
>已完结{{ detail.finishedCount }}</div>
</div>
</div>
......@@ -57,11 +57,11 @@
</div>
<!-- 筛选标签 -->
<div class="filter-tags" data-node-id="355:533">
<div class="filter-tags" data-node-id="355:533" v-if="activeTab == '3,4,5'">
<div
v-for="tag in filterTags"
:key="tag.id"
:class="['filter-tag', { active: tag.selected }]"
:class="['filter-tag', { active: tag.id==tagId }]"
:data-node-id="tag.nodeId"
@click="selectFilterTag(tag.id)"
>
......@@ -72,19 +72,18 @@
<!-- 商机列表 -->
<div class="business-list">
<div
v-for="(business, index) in filteredBusinessList"
v-for="(business, index) in detail.records"
:key="business.id"
class="business-card"
:data-node-id="business.nodeId"
@click="viewBusinessDetail(business)">
<div class="card-content">
<!-- 顶部编号和状态 -->
<div class="card-header" data-node-id="355:432">
<div class="business-number" data-node-id="355:433">
<div class="number-text" data-node-id="355:434">#{{ business.orderNumber }}</div>
<div class="number-text" data-node-id="355:434">{{ business.opportunityCode }}</div>
</div>
<div :class="['status-badge', business.statusClass]" data-node-id="355:435">
<span>{{ business.statusText }}</span>
<div :class="['status-badge', getStatusCalss(business.status)]" data-node-id="355:435">
<span>{{ business.statusName }}</span>
</div>
</div>
......@@ -94,7 +93,7 @@
<div class="detail-row" data-node-id="355:437">
<img class="detail-icon" src="images/location-icon.svg" alt="地址图标">
<div class="detail-text" data-node-id="355:441">
<span>{{ business.address }}</span>
<span>{{ addressShow(business.customerAddress) }}</span>
</div>
</div>
......@@ -102,7 +101,7 @@
<div class="detail-row contact-row" data-node-id="355:443">
<img class="detail-icon" src="images/user-icon.svg" alt="地址图标">
<div class="detail-text" data-node-id="355:446">
<span>{{ business.contactPhone }}</span>
<span>{{ business.customerPhone }}</span>
</div>
</div>
......@@ -111,33 +110,37 @@
<img class="detail-icon" src="images/tag-icon.svg" alt="地址图标">
<div class="business-tags" data-node-id="355:452">
<div
v-for="tag in business.tags"
:key="tag.id"
v-for="tag in business.tagNames"
class="business-tag"
:data-node-id="tag.nodeId"
>
{{ tag.name }}
{{ tag }}
</div>
</div>
</div>
<!-- 处理人信息 -->
<div class="processor-info" data-name="Container" data-node-id="355:457">
<span>处理人:{{ business.processor }}</span>
<span>处理人:{{ business.marketingStaffName }}</span>
</div>
<!-- 时间信息 -->
<div class="time-info" data-name="Container" data-node-id="355:459">
<div class="submit-time" data-name="Container" data-node-id="355:460">
<span>提交:{{ business.submitTime }}</span>
<span>提交:{{ switchTime(business.createTime) }}</span>
</div>
<div class="update-time">
<span>更新:{{ business.updateTime }}</span>
<span>更新:{{ switchTime(business.latestFollowTime)}}</span>
</div>
</div>
</div>
</div>
</div>
<div v-if="detail.records.length <= 0" style="text-align: center;font-size: .4rem;padding-top: 2rem;">暂无订单</div>
<div class="loginOut" @click="loginOut">
<img src="images/loginout.png" alt="">
<div>退出</div>
</div>
</div>
<!-- 底部导航 -->
......@@ -154,7 +157,7 @@
class="nav-item all-business active"
data-name="全部商机"
data-node-id="355:529">
<img class="nav-icon" src="images/business-icon.svg" alt="收集商机">
<img class="nav-icon" src="images/business-icon-active.svg" alt="收集商机">
<span class="nav-text" data-node-id="355:532">全部商机</span>
</div>
</div>
......@@ -162,6 +165,7 @@
</div>
<!-- 引入Vue.js -->
<script src="js/axios.min.js"></script>
<script src="js/vue.min.js"></script>
<script src="js/util.js"></script>
<script src="js/myBusi.js"></script>
......
......@@ -28,7 +28,7 @@
</div>
</div>
<div class="botButt" >发现商机</div>
<div class="botButt" @click="goAdd">发现商机</div>
<div class="botButt noBusi" @click="goBack">暂无商机</div>
</div>
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!