Squashed commit of the following:

commit 0e84bedb67
Author: 浅念 <email@sample.com>
Date:   Fri Aug 15 16:31:55 2025 +0800

    v1.0.2 更新内容

commit f5deb7b8f7
Author: 浅念 <email@sample.com>
Date:   Wed Aug 13 19:27:49 2025 +0800

    添加菜单 权限 等页面

commit e797cb24de
Author: 浅念 <email@sample.com>
Date:   Mon Aug 11 19:24:24 2025 +0800

    添加工单附件  及样式

commit 619311dfbe
Author: chenlan <suya123456>
Date:   Fri Aug 8 22:13:25 2025 +0800

    添加crm  归属客户

commit b402e522bf
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:51:17 2025 +0800

    询单权限校验

commit f950f665a9
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:10:45 2025 +0800

    添加 询单图片预览

commit 8deecc8197
Author: 浅念 <email@sample.com>
Date:   Wed Aug 6 18:36:42 2025 +0800

    修改用户页面 询单

commit 3e24d1905f
Author: 浅念 <email@sample.com>
Date:   Tue Aug 5 18:25:32 2025 +0800

    添加用户页面 客户页面接口 等

commit 7542d80e38
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 18:05:07 2025 +0800

    修改 仪表盘页面样式

commit e5548f95e4
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:50:55 2025 +0800

    修改询单字段  仪表盘页面  及添加管理端客户页面

commit d30ea5b7bf
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:48:58 2025 +0800

    添加echarts 插件

commit 0e4160c5ca
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:27:22 2025 +0800

    仪表盘接口

commit bd23288acd
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:26:46 2025 +0800

    修改 仪表盘

commit 6bfb940881
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 10:01:35 2025 +0800

    客户列表 添加公海详情展示

commit b50f650565
Author: 浅念 <email@sample.com>
Date:   Thu Jul 31 17:26:36 2025 +0800

    修改 客户标签样式  个人信息添加邮箱 修复关闭全部页面404

commit ea1dbb7af4
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 18:50:31 2025 +0800

    修改公海和询单页面

commit 1bfbb95bd9
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 17:23:09 2025 +0800

    修改样式和添加用户数据显示

commit a9f5f76b35
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 15:06:03 2025 +0800

    设置默认页面

commit 3ea9d3d5df
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 11:46:31 2025 +0800

    修改飞书登录 公司id 系统id 获取不到问题  客户及公海添加用户信息显示

commit 0cef82279f
Author: 浅念 <email@sample.com>
Date:   Tue Jul 29 16:42:36 2025 +0800

    修改工单 客户等页面

commit 695ebbcc71
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:50:04 2025 +0800

    修改默认值

commit ffc194a1c1
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:05:33 2025 +0800

    飞书授权添加系统码  及登录进入页面路由

commit 48b95e252f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:48:49 2025 +0800

    回收站添加占位

commit 382011d592
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:11:50 2025 +0800

    修改登录页样式公海数据 个人信息默认图

commit 0f790269db
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 09:16:55 2025 +0800

    修改字段名称

commit f18cfdb69e
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 17:28:51 2025 +0800

    调整代码格式

commit 858147fbbd
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 10:17:22 2025 +0800

    修改token刷新 及修改询单详情展示

commit c333b907e6
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 18:22:56 2025 +0800

    对接询单接口

commit 7979a31e36
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:40:02 2025 +0800

    修改代码格式

commit 4a39387865
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:07:13 2025 +0800

    设置客户列表权限

commit fc4de02582
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:08:13 2025 +0800

    feat(router): 调整菜单

commit 2ffe0c9753
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:04:16 2025 +0800

    feat(crmApi.ts): 调整结构

commit c3fb2c48d3
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:00:25 2025 +0800

    feat(router): 路由调整

commit c5b0a8265a
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:54:06 2025 +0800

    chore: 添加询单页面及调整菜单

commit ca06c9bc46
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:35:43 2025 +0800

    询单页面及菜单

commit 3b6de4c7fa
Merge: d3bb4ec 85e8b9b
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 09:22:20 2025 +0800

    Merge branch 'master' into feature/crm_pc

commit d3bb4ec6e5
Merge: c7d8908 d7ab6f1
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:22:55 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore
    #	src/api/crmApi.ts
    #	src/api/usersApi.ts
    #	src/store/modules/user.ts
    #	src/utils/http/index.ts
    #	src/views/auth/login/index.vue
    #	src/views/crmpc/customer/Show.vue
    #	src/views/crmpc/customer/index.vue

commit c7d890809c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:12:16 2025 +0800

    添加系统码

commit 31cd18fade
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:02:48 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit 59c9c97c08
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 17:56:52 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit d4b577c42e
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 13:03:56 2025 +0800

    feat(crm): 添加相关图片

commit e4967760b7
Merge: 0c67666 85102fc
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:25:49 2025 +0800

    Finish crm_pc

commit 85102fcf32
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:24:06 2025 +0800

    crm 接口对接

commit 0c67666793
Author: 浅念 <email@sample.com>
Date:   Wed Jul 16 17:18:14 2025 +0800

    添加上传头部信息

commit a41c75f703
Author: 浅念 <email@sample.com>
Date:   Tue Jul 15 09:40:53 2025 +0800

    修改页面

commit 22ae0e86c8
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 17:59:02 2025 +0800

    修改页面

commit 812e3c4b0c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 10:09:10 2025 +0800

    修改菜单问题

commit 2ad479b006
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:45:57 2025 +0800

    修改样式

commit 5802e157bf
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:43:34 2025 +0800

    修改工单

commit 713d43f7b4
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 18:37:16 2025 +0800

    添加登录

commit 719911918f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 4 17:16:38 2025 +0800

    项目创建

commit ce47b39fa5
Author: 浅念 <email@sample.com>
Date:   Wed Jul 2 09:37:51 2025 +0800

    删除
This commit is contained in:
浅念
2025-08-15 16:37:41 +08:00
parent 85e8b9b6b4
commit 5d906087d3
66 changed files with 13539 additions and 2803 deletions
+345
View File
@@ -0,0 +1,345 @@
<template>
<div class="customer-page">
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
</div>
</div>
<div class="boxshadow">
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
lass="customer-table"
>
<template #default>
<el-table-column prop="code" label="编号" align="center" />
<el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
<el-table-column prop="channel_name" label="渠道" align="center" />
<el-table-column prop="country_name" label="国家" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.follow)">
{{ scope.row.follow_txt }}
</span>
</template>
</el-table-column>
<el-table-column prop="user_name" label="标记人"></el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
<el-link
type="success"
underline="never"
style="margin-right: 8px"
@click="onDetail(scope.row)"
v-if="hasAuth('crm_customer@show')"
>
<i class="iconfont-sys"> &#xe70f;</i>详情
</el-link>
<el-link
type="primary"
underline="never"
style="margin-right: 8px"
@click="goaudit(scope.row)"
v-if="hasAuth('customer_audit@audet')"
>
<i class="iconfont-sys">&#xe6bf;</i> 审核
</el-link>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
</div>
<el-dialog v-model="detailDialogVisible" title="详情">
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<el-dialog v-model="audetDialogVisible" title="审核" width="360px">
<el-form :model="audetForm" label-width="0">
<el-form-item>
<el-select v-model="audetForm.scene" filterable placeholder="请选择" style="width: 100%">
<el-option label="彻底删除" value="delete" />
<el-option label="恢复至公海" value="report" />
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="audetDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAudetSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { useAuth } from '@/composables/useAuth'
import { manageService } from '@/api/adminApi'
const { hasAuth } = useAuth()
const keyword = ref<string>('')
const loading = ref(false)
const detailDialogVisible = ref(false)
const audetDialogVisible = ref(false)
const detailRow = ref<any>(null)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const audetForm = reactive({
id: '',
scene: ''
})
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
// 表格数据
const tableData = ref<any[]>([])
onMounted(() => {
getuserList()
gettableList()
})
watch([keyword], () => {
pagination.currentPage = 1
gettableList()
})
const gettableList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize,
scene: 'audit'
}
if (keyword.value) {
params.keyword = keyword.value
}
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
tableData.value = data.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
gettableList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
gettableList()
}
function statusClass(status: any) {
if (status === 0) return 'status-following'
if (status === 1) return 'status-dealed'
if (status === 2) return 'status-lost'
return ''
}
//审核
const goaudit = async (row: any) => {
audetForm.id = row.id
audetDialogVisible.value = true
}
const handleAudetSubmit = async () => {
const res = await manageService.crmCustomerAudit(audetForm)
ElMessage.success(res?.message || '设置成功!')
audetDialogVisible.value = false
gettableList()
}
function processImages(html: string) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
})
return doc.body.innerHTML
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
detailDialogVisible.value = true
}
</script>
<style scoped lang="scss">
:deep(.el-badge) {
--el-badge-size: 16px;
}
.itemtag {
margin-top: -16px;
margin-left: -13px;
}
:deep(.el-table__header th) {
background: #f7f9fa !important;
color: #333;
font-weight: bold;
}
:deep(.el-table__empty-block) {
height: 450px !important;
}
:deep(.el-table__body tr.custom-table-row td) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__body tr td:last-child) {
border-right: none !important;
}
:deep(.el-table__body tr:last-child td) {
border-bottom: none !important;
}
:deep(.el-table__header-wrapper th) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__header-wrapper th:last-child) {
border-right: none !important;
}
:deep(.el-table) {
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.customer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.customer-search-group {
display: flex;
gap: 12px;
}
.customer-tags {
background: var(--art-main-bg-color);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.filter-label {
width: 120px;
color: #666;
margin-right: 8px;
flex-shrink: 0;
font-size: 14px;
}
.filter-tags {
flex: 1;
color: #333;
font-weight: 500;
font-size: 14px;
}
.customer-tabs {
margin-bottom: 8px;
}
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.customer-table {
background: var(--art-main-bg-color);
border-radius: 8px;
margin-top: 0;
}
.filter-more-btn {
text-align: center;
margin-top: 8px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.tag-items {
cursor: pointer;
border-radius: 4px;
padding: 5px 10px;
transition: background 0.2s;
min-width: 100px;
display: inline-block;
margin-right: 30px;
}
.tag-item {
border-radius: 4px;
padding: 5px 10px;
}
.tag-itemall {
border-radius: 4px;
padding: 5px 10px;
margin-right: 20px;
}
.tag-active {
background: #3471ff;
color: #fff;
}
.tag-active :deep(.el-badge__content) {
color: #3471ff !important; /* 文字颜色 */
background-color: #fff !important; /* 背景色 */
}
</style>
+655
View File
@@ -0,0 +1,655 @@
<template>
<div class="customer-page">
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
</div>
</div>
<div class="boxshadow">
<el-button
type="primary"
@click="plzyDialogVisible = true"
v-if="hasAuth('highseas@allocation')"
:disabled="selectedRows.length === 0"
>
<i class="iconfont-sys">&#xe794;</i> 批量分配
</el-button>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
lass="customer-table"
>
<template #default>
<el-table-column type="selection" width="55px" align="center"></el-table-column>
<el-table-column prop="code" label="编号" align="center" />
<el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
<el-table-column prop="channel_name" label="渠道" align="center" />
<el-table-column prop="country_name" label="国家" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.follow)">
{{ scope.row.follow_txt }}
</span>
</template>
</el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<!-- <el-table-column prop="follow_next" label="下次跟进"></el-table-column> -->
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
<el-link
type="success"
underline="never"
style="margin-right: 8px"
@click="onDetail(scope.row)"
v-if="hasAuth('highseas@detail')"
>
<i class="iconfont-sys"> &#xe70f;</i>详情
</el-link>
<el-link
type="primary"
underline="never"
style="margin-right: 8px"
@click="gotransfer(scope.row)"
v-if="hasAuth('highseas@allocation')"
>
<i class="iconfont-sys"> &#xe794;</i>分配
</el-link>
<el-link
type="warning"
underline="never"
style="margin-right: 8px"
@click="gofollowdetail(scope.row)"
v-if="hasAuth('highseas@follow_detail')"
>
<i class="iconfont-sys"> &#xe64e;</i>跟进记录
</el-link>
<el-link
type="danger"
underline="never"
style="margin-right: 8px"
@click="onDelete(scope.row)"
v-if="hasAuth('highseas@delete')"
>
<i class="iconfont-sys">&#xe6d4;</i>删除
</el-link>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
</div>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
<div class="detail-info-table">
<div class="row">
<div>客户名称:{{ detailRow.contacts }}</div>
<div>电话:{{ detailRow.phone }}</div>
<div>国家:{{ detailRow.country_name }}</div>
</div>
<div class="row">
<div>渠道:{{ detailRow.channel_name }}</div>
<div>邮箱:{{ detailRow.email }}</div>
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
</div>
<div class="row">
<div>发布人:{{ detailRow.enter_user_name }}</div>
<div>录单时间:{{ detailRow.created_at }}</div>
<div>
等级:
<span>
{{ detailRow.magnitude_txt }}
</span>
</div>
</div>
<div class="row">
<div>
状态:<span>{{ detailRow.grade_txt }}</span>
</div>
<div>
跟进状态:<span>{{ detailRow.follow_txt }}</span>
</div>
</div>
</div>
<div class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<!-- 分配跟单对话框 -->
<el-dialog v-model="zyDialogVisible" title="分配" width="360px">
<el-form :model="zyForm" label-width="0">
<el-form-item>
<el-select
v-model="zyForm.user_id"
filterable
placeholder="请选择分配人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="zyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handlezySubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
<!-- 批量分配对话框 -->
<el-dialog v-model="plzyDialogVisible" title="批量分配" width="360px">
<el-form :model="plzyForm" label-width="0">
<el-form-item>
<el-select
v-model="plzyForm.user_id"
filterable
placeholder="请选择分配人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="plzyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
<!-- 跟进记录对话框 -->
<el-dialog v-model="followRecordDialogVisible" title="跟进记录" width="800px">
<!-- 跟进记录列表 -->
<div class="follow-record-list">
<ElScrollbar>
<template v-if="followRecords.length > 0">
<ElTimeline>
<ElTimelineItem
v-for="item in followRecords"
:key="item.time"
:placement="TIMELINE_PLACEMENT"
:color="activecolor"
:center="true"
>
<div class="timeline-item">
<div class="timeline-content-row">
<div class="timeline-content">
<p class="timeline-text"
>{{ item.type_txt }}
<span class="timeline-mom">{{ item.created_at }}</span></p
>
<p class="timeline-mom"
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
{{ item.audio_text }}</p
>
</div>
<img
v-if="item.picture_full"
:src="item.picture_full"
class="timeline-img"
alt="图片"
@click="handleImageClick(item.picture_full)"
style="cursor: pointer"
/>
</div>
</div>
</ElTimelineItem>
</ElTimeline>
</template>
<template v-else>
<!-- 使用 Element Plus 的空状态组件 -->
<el-empty :image="emptyimg" description="暂无跟进记录" />
</template>
</ElScrollbar>
</div>
</el-dialog>
<!-- 图片预览对话框 -->
<el-image-viewer
v-if="imageViewerVisible"
:url-list="imageList"
:initial-index="imageViewerIndex"
@close="closeImageViewer"
/>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import emptyimg from '@/assets/img/crm/empty.png'
const TIMELINE_PLACEMENT = 'top'
const activecolor = '#3471ff'
const { hasAuth } = useAuth()
const loading = ref(false)
const detailDialogVisible = ref(false)
const zyDialogVisible = ref(false)
const plzyDialogVisible = ref(false)
const followRecordDialogVisible = ref(false)
const detailRow = ref<any>(null)
const currentCustomer = ref<any>(null)
const followRecords = ref<any[]>([])
const keyword = ref<string>('')
// 图片预览相关
const imageViewerVisible = ref(false)
const imageList = ref<string[]>([])
const imageViewerIndex = ref(0)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const plzyForm = reactive({
customer_id: '',
user_id: ''
})
const zyForm = reactive({
customer_id: '',
user_id: ''
})
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
// 表格数据
const tableData = ref<any[]>([])
onMounted(() => {
getuserList()
gettableList()
})
watch([keyword], () => {
pagination.currentPage = 1
gettableList()
})
const gettableList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize,
scene: 'seas'
}
if (keyword.value) {
params.keyword = keyword.value
}
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
tableData.value = data.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
gettableList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
gettableList()
}
function statusClass(status: any) {
if (status === 0) return 'status-following'
if (status === 1) return 'status-dealed'
if (status === 2) return 'status-lost'
return ''
}
function processImages(html: string) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
})
return doc.body.innerHTML
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
detailDialogVisible.value = true
}
const gotransfer = (row: any) => {
zyForm.customer_id = row.id
zyDialogVisible.value = true
}
// 跟进记录
const gofollowdetail = async (row: any) => {
currentCustomer.value = row
followRecordDialogVisible.value = true
await manageService.crmCustomerFollow({ id: row.id }).then((res) => {
followRecords.value = res
})
}
// 图片预览相关函数
const handleImageClick = (url: string) => {
imageList.value = [url]
imageViewerIndex.value = 0
imageViewerVisible.value = true
}
const closeImageViewer = () => {
imageViewerVisible.value = false
imageList.value = []
imageViewerIndex.value = 0
}
// 分配
const handlezySubmit = async () => {
const res = await manageService.crmCustomerAllocation({
customer_id: zyForm.customer_id,
user_id: zyForm.user_id
})
ElMessage.success(res?.message || '分配成功!')
zyDialogVisible.value = false
gettableList()
}
// 选中的行数据
const selectedRows = ref<any[]>([])
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 批量分配
const handleplzySubmit = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
const res = await manageService.crmCustomerAllocation(plzyForm)
ElMessage.success(res?.message || '批量分配成功!')
plzyDialogVisible.value = false
gettableList()
}
//删除
const onDelete = async (row: any) => {
try {
const res = await manageService.crmCustomerDelete({ customer_id: row.id })
ElMessage.success(res?.message || '删除成功!')
gettableList()
} catch (error) {
ElMessage.error('删除失败,请稍后再试')
}
}
</script>
<style scoped lang="scss">
:deep(.el-badge) {
--el-badge-size: 16px;
}
.itemtag {
margin-top: -16px;
margin-left: -13px;
}
:deep(.el-table__header th) {
background: #f7f9fa !important;
color: #333;
font-weight: bold;
}
:deep(.el-table__empty-block) {
height: 450px !important;
}
:deep(.el-table__body tr.custom-table-row td) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__body tr td:last-child) {
border-right: none !important;
}
:deep(.el-table__body tr:last-child td) {
border-bottom: none !important;
}
:deep(.el-table__header-wrapper th) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__header-wrapper th:last-child) {
border-right: none !important;
}
:deep(.el-table) {
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.customer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.customer-search-group {
display: flex;
gap: 12px;
}
.customer-tags {
background: var(--art-main-bg-color);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.filter-label {
width: 120px;
color: #666;
margin-right: 8px;
flex-shrink: 0;
font-size: 14px;
}
.filter-tags {
flex: 1;
color: #333;
font-weight: 500;
font-size: 14px;
}
.customer-tabs {
margin-bottom: 8px;
}
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.customer-table {
background: var(--art-main-bg-color);
border-radius: 8px;
margin-top: 0;
}
.filter-more-btn {
text-align: center;
margin-top: 8px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.tag-items {
cursor: pointer;
border-radius: 4px;
padding: 5px 10px;
transition: background 0.2s;
min-width: 100px;
display: inline-block;
margin-right: 30px;
}
.tag-item {
border-radius: 4px;
padding: 5px 10px;
}
.tag-itemall {
border-radius: 4px;
padding: 5px 10px;
margin-right: 20px;
}
.tag-active {
background: #3471ff;
color: #fff;
}
.tag-active :deep(.el-badge__content) {
color: #3471ff !important; /* 文字颜色 */
background-color: #fff !important; /* 背景色 */
}
.detail-info-table {
padding-left: 15px;
.row {
display: flex;
justify-content: flex-start;
margin-bottom: 6px;
> div {
flex: 1;
min-width: 0;
margin-right: 16px;
overflow: hidden;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.dtitle {
line-height: 40px;
color: #333;
font-size: 16px;
font-weight: bold;
padding-left: 15px;
}
.detils-content {
padding-left: 15px;
img {
max-width: 100% !important;
}
}
.fenpei {
color: #3471ff !important;
}
.detil {
color: #00a43f !important;
}
.edit {
color: #008477 !important;
}
.delete {
color: #f00 !important;
}
.timeline-img {
width: 180px;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.follow-record-list {
padding: 15px;
max-height: 500px;
overflow-y: auto;
.timeline-item {
.timeline-content-row {
display: flex;
align-items: flex-start;
gap: 15px;
.timeline-content {
flex: 1;
.timeline-text {
font-size: 14px;
color: #303133;
margin-bottom: 5px;
.timeline-mom {
color: #909399;
font-size: 12px;
margin-left: 10px;
}
}
.timeline-mom {
font-size: 14px;
color: #606266;
line-height: 1.6;
}
}
}
}
}
</style>
+828
View File
@@ -0,0 +1,828 @@
<template>
<div class="customer-page">
<!-- 顶部操作栏 -->
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
<el-form-item label="跟单人" label-width="70px">
<el-select
v-model="selectValue"
style="min-width: 120px"
filterable
multiple
collapse-tags
placeholder="请选择跟单人"
>
<el-option v-for="v in userOptions" :key="v.id" :label="v.name" :value="v.id">
</el-option>
</el-select>
</el-form-item>
</div>
<el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
<i class="iconfont-sys">&#xe89f;</i> 导出
</el-button>
</div>
<!-- 筛选标签 -->
<div class="customer-tags boxshadow">
<!-- 前3组 -->
<div class="filter-row" v-for="item in visibleTagGroups" :key="item.id">
<span class="filter-label">{{ item.name }}</span>
<div class="filter-tags">
<span
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
@click="handleTagClick(item.id, '')"
>全部</span
>
<span
class="tag-items"
v-for="item1 in item.tags"
:key="item1.id"
@click="handleTagClick(item.id, item1.id)"
>
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
{{ item1.name }}
</span>
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
</span>
</div>
</div>
<!-- 更多 -->
<transition name="fade">
<div v-if="showMore">
<div class="filter-row" v-for="item in moreTagGroups" :key="item.id">
<span class="filter-label">{{ item.name }}</span>
<div class="filter-tags">
<span
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
@click="handleTagClick(item.id, '')"
>全部</span
>
<span
class="tag-items"
v-for="item1 in item.tags"
:key="item1.id"
@click="handleTagClick(item.id, item1.id)"
>
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
{{ item1.name }}
</span>
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
</span>
</div>
</div>
</div>
</transition>
<div class="filter-more-btn" v-if="moreTagGroups.length">
<el-link underline="never" @click="showMore = !showMore">
{{ showMore ? '收起' : '更多' }}
<i v-if="showMore" class="iconfont-sys"> &#xe864;</i>
<i v-else class="iconfont-sys">&#xe867;</i>
</el-link>
</div>
</div>
<div class="boxshadow">
<el-button
type="primary"
@click="plzyDialogVisible = true"
v-if="hasAuth('customer@transfer')"
:disabled="selectedRows.length === 0"
>
<i class="iconfont-sys">&#xe794;</i> 批量转移
</el-button>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
lass="customer-table"
>
<template #default>
<el-table-column type="selection" width="55px" align="center"></el-table-column>
<el-table-column prop="code" label="编号" align="center" />
<el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
<el-table-column prop="channel_name" label="渠道" align="center" />
<el-table-column prop="country_name" label="国家" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.follow)">
{{ scope.row.follow_txt }}
</span>
</template>
</el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column prop="user_name" label="跟单人"></el-table-column>
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
<el-link
type="success"
underline="never"
style="margin-right: 8px"
@click="onDetail(scope.row)"
v-if="hasAuth('customer@detail')"
>
<i class="iconfont-sys"> &#xe70f;</i>详情
</el-link>
<el-link
type="primary"
underline="never"
style="margin-right: 8px"
@click="gotransfer(scope.row)"
v-if="hasAuth('customer@transfer')"
>
<i class="iconfont-sys"> &#xe794;</i>转移
</el-link>
<el-link
type="warning"
underline="never"
style="margin-right: 8px"
@click="gofollowdetail(scope.row)"
v-if="hasAuth('customer@follow_detail')"
>
<i class="iconfont-sys"> &#xe64e;</i>跟进记录
</el-link>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
</div>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
<div class="detail-info-table">
<div class="row">
<div>客户名称:{{ detailRow.contacts }}</div>
<div>电话:{{ detailRow.phone }}</div>
<div>国家:{{ detailRow.country_name }}</div>
</div>
<div class="row">
<div>渠道:{{ detailRow.channel_name }}</div>
<div>邮箱:{{ detailRow.email }}</div>
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
</div>
<div class="row">
<div>发布人:{{ detailRow.enter_user_name }}</div>
<div>跟单人:{{ detailRow.user_name }}</div>
<div>录单时间:{{ detailRow.created_at }}</div>
</div>
<div class="row">
<div>
等级:
<span>
{{ detailRow.magnitude_txt }}
</span>
</div>
<div>
状态:<span>{{ detailRow.grade_txt }}</span>
</div>
<div>
跟进状态:<span>{{ detailRow.follow_txt }}</span>
</div>
</div>
</div>
<div class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<!-- 转移跟单对话框 -->
<el-dialog v-model="zyDialogVisible" title="转移" width="360px">
<el-form :model="zyForm" label-width="0">
<el-form-item>
<el-select
v-model="zyForm.user_id"
filterable
placeholder="请选择转移人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="zyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handlezySubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
<!-- 批量转移对话框 -->
<el-dialog v-model="plzyDialogVisible" title="批量转移" width="360px">
<el-form :model="plzyForm" label-width="0">
<el-form-item>
<el-select
v-model="plzyForm.user_id"
filterable
placeholder="请选择转移人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="plzyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
<!-- 跟进记录对话框 -->
<el-dialog v-model="followRecordDialogVisible" title="跟进记录" width="800px">
<!-- 跟进记录列表 -->
<div class="follow-record-list">
<ElScrollbar>
<template v-if="followRecords.length > 0">
<ElTimeline>
<ElTimelineItem
v-for="item in followRecords"
:key="item.time"
:placement="TIMELINE_PLACEMENT"
:color="activecolor"
:center="true"
>
<div class="timeline-item">
<div class="timeline-content-row">
<div class="timeline-content">
<p class="timeline-text"
>{{ item.type_txt }}
<span class="timeline-mom">{{ item.created_at }}</span></p
>
<p class="timeline-mom"
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
{{ item.audio_text }}</p
>
</div>
<img
v-if="item.picture_full"
:src="item.picture_full"
class="timeline-img"
alt="图片"
@click="handleImageClick(item.picture_full)"
style="cursor: pointer;"
/>
</div>
</div>
</ElTimelineItem>
</ElTimeline>
</template>
<template v-else>
<!-- 使用 Element Plus 的空状态组件 -->
<el-empty :image="emptyimg" description="暂无跟进记录" />
<!-- <el-empty /> -->
<!-- 或者自定义图片 -->
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
</template>
</ElScrollbar>
</div>
</el-dialog>
<!-- 图片预览对话框 -->
<el-image-viewer
v-if="imageViewerVisible"
:url-list="imageList"
:initial-index="imageViewerIndex"
@close="closeImageViewer"
/>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { CrmService } from '@/api/crmApi'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import emptyimg from '@/assets/img/crm/empty.png'
const TIMELINE_PLACEMENT = 'top'
const activecolor = '#3471ff'
const { hasAuth } = useAuth()
const selectValue = ref<string[]>([])
const showMore = ref(false)
const loading = ref(false)
const detailDialogVisible = ref(false)
const zyDialogVisible = ref(false)
const plzyDialogVisible = ref(false)
const followRecordDialogVisible = ref(false)
const detailRow = ref<any>(null)
const currentCustomer = ref<any>(null)
const followRecords = ref<any[]>([])
const keyword = ref<string>('')
// 图片预览相关
const imageViewerVisible = ref(false)
const imageList = ref<string[]>([])
const imageViewerIndex = ref(0)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const plzyForm = reactive({
customer_id: '',
user_id: ''
})
const zyForm = reactive({
customer_id: '',
user_id: ''
})
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
// 获取列表数据
const tagActiveMap = reactive<{ [groupId: string]: string }>({})
//获取标签
type TagGroup = { id: string; name: string; tags: { id: string; name: string; count: number }[] }
const tagGroups = ref<TagGroup[]>([])
const tagGrouplistfn = async () => {
const res = await CrmService.tagGrouplist({ scene: 'customers' })
tagGroups.value = res || []
tagGroups.value.forEach((group) => {
if (group.tags && Array.isArray(group.tags)) {
tagActiveMap[group.id] = ''
}
})
}
// 计算前3组和更多组
// eslint-disable-next-line vue/valid-define-props
defineProps()
const visibleTagGroups = computed(() => tagGroups.value.slice(0, 3))
const moreTagGroups = computed(() => tagGroups.value.slice(3))
// 表格数据
const tableData = ref<any[]>([])
onMounted(() => {
getuserList()
gettableList()
tagGrouplistfn()
})
// tabs 切换联动
watch([selectValue, keyword], () => {
pagination.currentPage = 1
gettableList()
})
const gettableList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize
}
if (selectValue.value && selectValue.value.length) {
params.user_id = selectValue.value.join(',')
}
if (keyword.value) {
params.keyword = keyword.value
}
// 标签参数,所有选中的标签id组成数组
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
if (selectedTagIds.length) {
params.tag_id = selectedTagIds
}
console.log(params)
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
tableData.value = data.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
gettableList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
gettableList()
}
function statusClass(status: any) {
if (status === 0) return 'status-following'
if (status === 1) return 'status-dealed'
if (status === 2) return 'status-lost'
return ''
}
//导出
const exportfn = () => {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize,
user_id: selectValue.value.join(','),
tag_id: Object.values(tagActiveMap).filter(Boolean)
}
// manageService.crmCustomerExport(params)
try {
// 1. 调用接口,获取blob
// const res = await manageService.crmCustomerExport(params)
const urls =
import.meta.env.VITE_API_URL +
`/admin/crm/customer/export?current=${params.currentPage}&size=${params.pageSize}&user_id=${params.user_id}&tag_id=${params.tag_id}`
// 2. 创建blob对象
// const blob = new Blob([res], { type: 'application/vnd.ms-excel' }) // 或根据后端实际类型调整
// 3. 创建下载链接
const url = urls
const link = document.createElement('a')
link.href = url
link.download = '客户数据.xlsx' // 可自定义文件名
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (e) {
ElMessage.error('导出失败')
}
}
// 标签选中状态,key为组id,值为选中的标签id
function handleTagClick(groupId: string, tagId: string) {
// if (tagActiveMap[groupId] === tagId) {
// tagActiveMap[groupId] = '' // 取消选中
// } else {
tagActiveMap[groupId] = tagId // 选中
// }
pagination.currentPage = 1
gettableList()
}
function processImages(html: string) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
})
return doc.body.innerHTML
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
detailDialogVisible.value = true
}
const gotransfer = (row: any) => {
zyForm.customer_id = row.id
zyDialogVisible.value = true
}
// 跟进记录
const gofollowdetail = async (row: any) => {
currentCustomer.value = row
followRecordDialogVisible.value = true
await manageService.crmCustomerFollow({ id: row.id }).then((res) => {
followRecords.value = res
})
}
// 图片预览相关函数
const handleImageClick = (url: string) => {
imageList.value = [url]
imageViewerIndex.value = 0
imageViewerVisible.value = true
}
const closeImageViewer = () => {
imageViewerVisible.value = false
imageList.value = []
imageViewerIndex.value = 0
}
// 转移
const handlezySubmit = async () => {
const res = await manageService.crmCustomerTransfer({
customer_id: zyForm.customer_id,
user_id: zyForm.user_id
})
ElMessage.success(res?.message || '转移成功!')
zyDialogVisible.value = false
gettableList()
}
// 选中的行数据
const selectedRows = ref<any[]>([])
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 批量转移
const handleplzySubmit = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
const res = await manageService.crmCustomerTransfer(plzyForm)
ElMessage.success(res?.message || '批量转移成功!')
plzyDialogVisible.value = false
gettableList()
}
</script>
<style scoped lang="scss">
.timeline-img {
width: 180px;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
:deep(.el-badge) {
--el-badge-size: 16px;
}
.itemtag {
margin-top: -16px;
margin-left: -13px;
}
:deep(.el-table__header th) {
background: #f7f9fa !important;
color: #333;
font-weight: bold;
}
:deep(.el-table__empty-block) {
height: 450px !important;
}
:deep(.el-table__body tr.custom-table-row td) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__body tr td:last-child) {
border-right: none !important;
}
:deep(.el-table__body tr:last-child td) {
border-bottom: none !important;
}
:deep(.el-table__header-wrapper th) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__header-wrapper th:last-child) {
border-right: none !important;
}
:deep(.el-table) {
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.customer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.customer-search-group {
display: flex;
gap: 12px;
}
.customer-tags {
background: var(--art-main-bg-color);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.filter-label {
width: 120px;
color: #666;
margin-right: 8px;
flex-shrink: 0;
font-size: 14px;
}
.filter-tags {
flex: 1;
color: #333;
font-weight: 500;
font-size: 14px;
}
.customer-tabs {
margin-bottom: 8px;
}
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.customer-table {
background: var(--art-main-bg-color);
border-radius: 8px;
margin-top: 0;
}
.filter-more-btn {
text-align: center;
margin-top: 8px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.tag-items {
cursor: pointer;
border-radius: 4px;
padding: 5px 10px;
transition: background 0.2s;
min-width: 100px;
display: inline-block;
margin-right: 30px;
}
.tag-item {
border-radius: 4px;
padding: 5px 10px;
}
.tag-itemall {
border-radius: 4px;
padding: 5px 10px;
margin-right: 20px;
}
.tag-active {
background: #3471ff;
color: #fff;
}
.tag-active :deep(.el-badge__content) {
color: #3471ff !important; /* 文字颜色 */
background-color: #fff !important; /* 背景色 */
}
.detail-info-table {
padding-left: 15px;
.row {
display: flex;
justify-content: flex-start;
margin-bottom: 6px;
> div {
flex: 1;
min-width: 0;
margin-right: 16px;
overflow: hidden;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.dtitle {
line-height: 40px;
color: #333;
font-size: 16px;
font-weight: bold;
padding-left: 15px;
}
.detils-content {
padding-left: 15px;
img {
max-width: 100% !important;
}
}
.follow-record-header {
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
margin-bottom: 15px;
.customer-info {
display: flex;
align-items: center;
.label {
font-size: 14px;
color: #606266;
margin-right: 5px;
}
.value {
font-size: 14px;
color: #303133;
font-weight: bold;
}
}
}
.follow-record-list {
padding: 15px;
max-height: 500px;
overflow-y: auto;
.list-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 15px;
padding-left: 10px;
}
.record-item {
background-color: #f5f7fa;
border-radius: 4px;
padding: 10px 15px;
margin-bottom: 10px;
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.record-type {
font-size: 14px;
color: #3471ff;
font-weight: bold;
background-color: #e6f7ff;
padding: 2px 8px;
border-radius: 4px;
}
.record-time {
font-size: 14px;
color: #909399;
}
.record-user {
font-size: 14px;
color: #3471ff;
font-weight: bold;
}
}
.record-content {
font-size: 14px;
color: #303133;
line-height: 1.6;
margin-bottom: 5px;
}
.record-footer {
.next-follow {
font-size: 14px;
color: #909399;
}
}
}
.no-records {
text-align: center;
color: #909399;
padding: 20px 0;
}
}
</style>
+906
View File
@@ -0,0 +1,906 @@
<!-- eslint-disable vue/no-parsing-error -->
<!-- eslint-disable vue/valid-attribute-name -->
<!-- eslint-disable prettier/prettier -->
<template>
<div class="boxshadow">
<section class="search-bar art-custom-card">
<el-form v-model:filter="formFilters" label-position="left">
<el-row class="search-form-row" :gutter="12">
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
<el-form-item label="关键字" label-width="70px">
<el-input v-model="filter.keyword" placeholder="请输入关键字" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
<el-form-item label="状态" label-width="70px">
<el-select v-model="filter.status">
<el-option label="全部" value="" />
<el-option label="待发布" value="0" />
<el-option label="待跟踪" value="1" />
<el-option label="已跟踪" value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
<el-form-item label="渠道" label-width="70px">
<el-select v-model="filter.channel">
<el-option v-for="v in channelOptions" :key="v.id" :label="v.name" :value="v.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<el-form-item label="时间" label-width="70px">
<el-date-picker
v-model="filter.time"
type="daterange"
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
rangeSeparator="至"
startPlaceholder="开始日期"
endPlaceholder="结束日期"
:style="{ width: '100%' }"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="4" :lg="4" :xl="4" class="action-column">
<div
class="action-buttons-wrapper"
:style="{
'justify-content': isMobile ? 'flex-end' : 3 <= 2 ? 'flex-start' : 'flex-end'
}"
>
<div class="form-buttons">
<el-button class="reset-button" @click="handleReset">{{
$t('table.searchBar.reset')
}}</el-button>
<el-button type="primary" class="search-button" @click="handleSearch">{{
$t('table.searchBar.search')
}}</el-button>
</div>
<div class="form-btn-right">
</div>
</div>
</el-col>
</el-row>
</el-form>
</section>
<div class="table">
<el-button class="reset-button" @click="addfn()" v-if="hasAuth('inquiry#store')"><i class="iconfont-sys">&#xe602;</i> 录单</el-button>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<el-table-column prop="order_no" label="SN"></el-table-column>
<el-table-column prop="contacts" label="客户名称"></el-table-column>
<el-table-column prop="tel" label="电话"></el-table-column>
<el-table-column prop="phone" label="国家">
<template #default="scope">
<span>
{{scope.row.countryInfo? scope.row.countryInfo.cn_addr : ''}}
</span>
<span v-if="scope.row.countryInfo">
<el-tag class="steer" type="warning" v-if="scope.row.countryInfo.steering=='左舵'">{{ scope.row.countryInfo.steering }}</el-tag>
<el-tag class="steer" type="success" v-else-if="scope.row.countryInfo.steering=='右舵'">{{ scope.row.countryInfo.steering }}</el-tag>
</span>
<!-- <span class="steer" v-if="scope.row.countryInfo">{{ scope.row.countryInfo.steering }}</span> -->
</template>
</el-table-column>
<el-table-column prop="phone" label="渠道">
<template #default="scope">
<span>
{{scope.row.channelInfo? scope.row.channelInfo.name : ''}}
</span>
</template>
</el-table-column>
<el-table-column prop="quality" label="客户商机">
<template #default="scope">
<span :style="scope.row.quality==1 ? { color: '#F5AB00' } : scope.row.quality==2 ? { color: '#8495B8' }: { color: '#9B7200' }">
{{scope.row.quality==1 ? '金牌商机' : scope.row.quality==2 ? '银牌商机' : '铜牌商机'}}
</span>
</template>
</el-table-column>
<el-table-column prop="magnitude" label="发布人">
<template #default="scope">
<span>
{{scope.row.enter? scope.row.enter.name : ''}}
</span>
</template>
</el-table-column>
<el-table-column prop="recipient" label="跟单人">
<template #default="scope">
<span style="color:#008477;cursor: pointer;" v-if="scope.row.recipientInfo && hasAuth('inquiry#reallocation')" @click="onFenpei(scope.row,'edit')">
{{scope.row.recipientInfo? scope.row.recipientInfo.name : ''}}
<i class="iconfont-sys">&#xe626;</i>
</span>
</template>
</el-table-column>
<el-table-column label="状态">
<template #default="scope">
<span :style="scope.row.status== 0 ? { color: '#ccc' } :scope.row.status== 1 ? { color: '#00A43F' }: { color: '#3471FF' }">
{{ scope.row.status== 0 ? '未发布' : scope.row.status== 1 ? '待跟踪' : '已跟踪' }}
</span>
</template>
</el-table-column>
<!-- <el-table-column label="倒计时" min-width="140">
<template #default="scope">
<span :class="getCountdownClass(scope.row.countdown)" v-if="scope.row.status==1">
<span class="custom-clock-icon" style="margin-right: 4px; vertical-align: middle"
><i class="iconfont-sys">&#xe7b1;</i></span
>
{{ getCountdown(scope.row.countdown) }}
</span>
</template>
</el-table-column> -->
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="260px">
<template #default="scope">
<div class="follow-action fenpei" @click="onFenpei(scope.row,'add')" v-if="scope.row.status!= 2&&hasAuth('inquiry#allocation')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe852;</i></span
>
分配
</div>
<div class="follow-action detil" @click="onDetail(scope.row)" v-if="hasAuth('inquiry#show')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe70f;</i></span
>
详情
</div>
<div class="follow-action" @click="onFollow(scope.row)" v-if="scope.row.status==0&&hasAuth('inquiry#release')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe712;</i></span
>
发布
</div>
<div class="follow-action edit" @click="onEdit(scope.row)" v-if="hasAuth('inquiry#update')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe6bf;</i></span
>
编辑
</div>
<div class="follow-action delete" @click="onDelete(scope.row)" v-if="hasAuth('inquiry#destroy')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe6d4;</i></span
>
删除
</div>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
</div>
<el-dialog v-model="detailDialogVisible" title="详情" >
<div class="detail-info-table">
<div class="row">
<div>客户名称:{{ detailRow.contacts }}</div>
<div>电话:{{ detailRow.tel }}</div>
<div>国家:{{ detailRow.countryInfo? detailRow.countryInfo.cn_addr:'' }}</div>
</div>
<div class="row">
<div>渠道:{{ detailRow.channelInfo?detailRow.channelInfo.name:'' }}</div>
<div>邮箱:{{ detailRow.email }}</div>
<div>WhatsApp:{{ detailRow.whatsapp }}</div>
</div>
<div class="row">
<div>发布人:{{ detailRow.enter? detailRow.enter.name : ''}}</div>
<div>跟单人:{{ detailRow.recipientInfo? detailRow.recipientInfo.name : ''}}</div>
<div>录单时间:{{ detailRow.created_at }}</div>
</div>
<div class="row">
<div>
等级:
<span :style="detailRow.quality==1 ? { color: '#F5AB00' } : detailRow.quality==2 ? { color: '#8495B8' }: { color: '#9B7200' }">
{{detailRow.quality==1 ? '金牌商机' : detailRow.quality==2 ? '银牌商机' : '铜牌商机'}}
</span>
</div>
<div
>状态:<span>{{ detailRow.status== 0 ? '未发布' : detailRow.status== 1 ? '待跟踪' : '已跟踪' }}</span></div
>
<div>SN:{{ detailRow.order_no }}</div>
</div>
</div>
<div class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.content" class="detils-content" @click="handleImageClick" style="cursor: pointer;">
</div>
<!-- 图片预览组件 -->
<ElImageViewer
v-if="showImageViewer"
:url-list="imageList"
:initial-index="currentImageIndex"
:hide-on-click-modal="true"
@close="showImageViewer = false"
/>
</el-dialog>
<el-dialog
:title="dialogType === 'add' ? '录单' : '编辑'"
v-model="AddDialogVisible"
width="800px"
:close-on-click-modal="false"
>
<el-form :model="addForm" ref="addFormRef" label-width="80px" class="add-form">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="渠道" prop="channel">
<el-select v-model="addForm.channel" placeholder="请选择渠道">
<el-option
v-for="item in channelOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="等级" prop="quality">
<el-select v-model="addForm.quality" placeholder="请选择等级">
<el-option
v-for="item in opportunityOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="国家" prop="country">
<el-select v-model="addForm.country" filterable placeholder="请选择国家">
<el-option
v-for="item in countryOptions"
:key="item.id"
:label="item.cn_addr"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Email" prop="email">
<el-input v-model="addForm.email" placeholder="请输入Email" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="联系方式" prop="tel">
<el-input v-model="addForm.tel" placeholder="请输入联系方式" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人" prop="contacts">
<el-input v-model="addForm.contacts" placeholder="请输入联系人" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="WhatsApp" prop="whatsapp">
<el-input v-model="addForm.whatsapp" placeholder="请输入WhatsApp" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="跟单人" prop="country">
<el-select v-model="addForm.recipient" filterable placeholder="请选择跟单人">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="详情内容" prop="content">
<ArtWangEditor class="el-top" v-model="editorHtml" />
</el-form-item>
<div class="form-submit-row">
<el-button type="primary" size="large" style="width: 50%" @click="handleAddSubmit"
>提交</el-button
>
</div>
</el-form>
</el-dialog>
<!-- 分配 -->
<el-dialog v-model="fenpeiDialogVisible" :title="dialogTypefp === 'add' ? '分配跟单' : '重新分配跟单'" width="360px">
<el-form :model="fenpeiForm" label-width="0">
<el-form-item>
<el-select v-model="fenpeiForm.recipient" filterable placeholder="请选择" style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="fenpeiDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleFenpeiSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import dayjs from 'dayjs'
import { ElMessageBox, ElImageViewer } from 'element-plus'
import { CrmService } from '@/api/crmApi'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false)
const dialogType = ref('add')
const dialogTypefp = ref('add')
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const filter = ref({
status: '',
time: '',
channel: '',
keyword: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const editorHtml = ref('') // 编辑器内容
const timer = ref(0)
const refreshKey = ref(0)
const fenpeiDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const AddDialogVisible = ref(false)
const detailRow = ref<any>(null)
const countryOptions = ref<any>()
const tableData = ref<any[]>([])
const addFormRef = ref()
let addForm = reactive({
channel: '',
quality: '',
country: '',
email: '',
tel: '',
contacts: '',
whatsapp: '',
content: '',
recipient: ''
})
// 响应式表单数据
const formFilters = reactive({ ...filter })
// 重置表单
const handleReset = () => {
filter.value = {
status: '',
time: '',
channel: '',
keyword: ''
}
Object.assign(formFilters, { ...filter })
pagination.currentPage = 1 // 重置到第一页
gettableList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1 // 搜索时重置到第一页
gettableList()
}
const opportunityOptions = ref<any>()
//商机
const getMagnitude = async () => {
// 这里调用获取品牌列表的接口
const res = await CrmService.getMagnitude()
opportunityOptions.value = res || []
}
const channelOptions = ref<any>()
//渠道
const getgetChannel = async () => {
const res = await CrmService.getChannel()
channelOptions.value = res
}
//国家
const getarea = async () => {
const res = await CrmService.getCountry()
countryOptions.value = res || []
}
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
// 计算属性:将时间格式化为 "开始日期 - 结束日期"
const formattedTimeRange = () => {
if (!filter.value.time || filter.value.time.length !== 2) {
return '' // 如果时间范围无效,返回空字符串
}
const [startDate, endDate] = filter.value.time
return `${startDate} - ${endDate}`
}
// 获取列表数据
const gettableList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize,
channel: filter.value.channel,
status: filter.value.status,
keyword: filter.value.keyword,
time: formattedTimeRange()
}
const { records, current, size, total } = await manageService.getinquiryList(params)
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 选中的行数据
const selectedRows = ref<any[]>([])
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
gettableList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
gettableList()
}
onMounted(() => {
getgetChannel()
gettableList()
getuserList()
timer.value = window.setInterval(() => {
refreshKey.value++
}, 1000)
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
function getCountdown(targetTimestamp: number) {
// 让 refreshKey 成为依赖,确保每秒刷新
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
refreshKey.value
const now = dayjs()
const target = dayjs.unix(targetTimestamp) // 使用时间戳创建 dayjs 对象
if (!target.isValid()) return '时间格式错误'
const diff = target.diff(now)
if (diff <= 0) return '已超时'
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
const minutes = Math.floor((diff / (1000 * 60)) % 60)
const seconds = Math.floor((diff / 1000) % 60)
return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
}
// function getCountdown(targetDate: string) {
// // 让 refreshKey 成为依赖,确保每秒刷新
// // eslint-disable-next-line @typescript-eslint/no-unused-expressions
// refreshKey.value
// const now = dayjs()
// const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
// if (!target.isValid()) return '时间格式错误'
// const diff = target.diff(now)
// if (diff <= 0) return '已超时'
// const days = Math.floor(diff / (1000 * 60 * 60 * 24))
// const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
// const minutes = Math.floor((diff / (1000 * 60)) % 60)
// const seconds = Math.floor((diff / 1000) % 60)
// return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
// }
function getCountdownClass(targetDate: number) {
const now = dayjs()
// const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
const target = dayjs.unix(targetDate)
if (!target.isValid() || target.diff(now) <= 0) {
return 'countdown-danger'
}
return 'countdown-success'
}
function reactiveForm() {
Object.assign(addForm, {
channel: '',
quality: '',
country: '',
email: '',
tel: '',
contacts: '',
whatsapp: '',
content: '',
recipient: ''
})
editorHtml.value = ''
}
const addfn = () => {
dialogType.value = 'add'
reactiveForm()
getMagnitude()
getarea()
AddDialogVisible.value = true
}
const onFollow = async (row: any) => {
ElMessageBox.confirm('确定要发布此询单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await manageService.inquiryrelease({ id: row.id, status: 1 })
ElMessage.success(res?.message || '请求成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
function onDetail(row: any) {
row.content = processImages(row.content) // 处理图片
detailRow.value = row
detailDialogVisible.value = true
}
//编辑
function processImages(html: string) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
})
return doc.body.innerHTML
}
const carrid = ref('')
const onEdit = async (row: any) => {
dialogType.value = 'edit'
getMagnitude()
getarea()
AddDialogVisible.value = true
carrid.value = row.id
addForm = reactive({
channel: row.channelInfo ? row.channelInfo.id : '',
quality: row.quality,
country: row.countryInfo ? row.countryInfo.id : '',
email: row.email,
tel: row.tel,
contacts: row.contacts,
whatsapp: row.whatsapp,
content: row.content,
recipient: row.recipientInfo ? row.recipientInfo.id : ''
})
editorHtml.value = row.content
// 这里可以添加编辑逻辑
// 例如打开编辑对话框或跳转到编辑页面
}
const delCodeTrim = (content: string): string => {
return content.replace(/(\s*)<\/code>/g, '</code>')
}
const handleAddSubmit = async () => {
// 提交表单逻辑
// 这里可以调用 API 提交 addForm 数据
if (dialogType.value === 'add') {
editorHtml.value = delCodeTrim(editorHtml.value)
addForm.content = editorHtml.value
const res = await manageService.inquiryAdd(addForm)
ElMessage.success(res?.message || '录单成功!')
} else {
editorHtml.value = delCodeTrim(editorHtml.value)
addForm.content = editorHtml.value
const forms = { ...addForm, id: carrid.value }
const res = await manageService.inquiryedit(forms)
ElMessage.success(res?.message || '录单成功!')
}
AddDialogVisible.value = false
gettableList()
}
//删除
const onDelete = (row: any) => {
ElMessageBox.confirm('确定要删除此条询单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await manageService.inquirydelete({ id: row.id })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
const fenpeiForm = reactive({
id: '',
recipient: ''
})
//分配
const onFenpei = (row: any, edits: any) => {
dialogTypefp.value = edits
fenpeiForm.id = row.id
fenpeiDialogVisible.value = true
}
const handleFenpeiSubmit = async () => {
if (!fenpeiForm.recipient) {
ElMessage.warning('请选择跟单人')
return
}
if (dialogTypefp.value === 'add') {
const res = await manageService.inquiryallocation({
id: fenpeiForm.id,
recipient: fenpeiForm.recipient,
status: 2
})
ElMessage.success(res?.message || '分配成功!')
} else {
const res = await manageService.inquiryreallocation({
id: fenpeiForm.id,
recipient: fenpeiForm.recipient,
status: 2
})
ElMessage.success(res?.message || '分配成功!')
}
fenpeiDialogVisible.value = false
gettableList()
}
const imageList = ref<string[]>([])
const currentImageIndex = ref(0)
const showImageViewer = ref(false)
const handleImageClick = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.tagName === 'IMG') {
const imgElement = target as HTMLImageElement
// 获取所有图片并设置预览列表
const contentDiv = event.currentTarget as HTMLElement
const allImages = contentDiv.querySelectorAll('img')
const imageUrls = Array.from(allImages).map(img => img.src)
imageList.value = imageUrls
currentImageIndex.value = imageUrls.indexOf(imgElement.src)
showImageViewer.value = true
}
}
</script>
<style lang="scss" scoped>
.search-bar {
padding: 20px 20px 0;
background-color: var(--art-main-bg-color);
border-radius: calc(var(--custom-radius) / 2 + 2px);
:deep(.el-form-item__label) {
display: flex;
align-items: center;
line-height: 20px;
}
.search-form-row {
display: flex;
flex-wrap: wrap;
}
.action-column {
flex: 1;
max-width: 100%;
.action-buttons-wrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
// justify-content: flex-end;
margin-bottom: 12px;
}
.filter-toggle {
display: flex;
align-items: center;
margin-left: 10px;
line-height: 32px;
color: var(--main-color);
cursor: pointer;
span {
font-size: 14px;
user-select: none;
}
.icon-wrapper {
display: flex;
align-items: center;
margin-left: 4px;
font-size: 14px;
}
}
.form-btn-right {
flex: 1;
text-align: right;
}
}
}
.dark {
.card-list {
.art-custom-card {
.iconfont-sys {
background-color: #232323 !important;
}
}
}
}
.table {
margin-top: 10px;
:deep(.el-table__empty-block) {
height: 300px !important;
}
:deep(.el-table__header th) {
font-weight: bold;
color: #333;
background: #f7f9fa !important;
}
:deep(.el-table__body tr.custom-table-row td) {
border-right: 1px solid #f7f9fa !important;
border-bottom: 1px solid #f7f9fa !important;
}
:deep(.el-table__body tr td:last-child) {
border-right: none !important;
}
:deep(.el-table__body tr:last-child td) {
border-bottom: none !important;
}
:deep(.el-table__header-wrapper th) {
border-right: 1px solid #f7f9fa !important;
border-bottom: 1px solid #f7f9fa !important;
}
:deep(.el-table__header-wrapper th:last-child) {
border-right: none !important;
}
:deep(.el-table) {
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
:deep(.el-table th),
:deep(.el-table td) {
text-align: center !important;
}
}
.countdown-danger {
color: #f00 !important;
}
.countdown-success {
color: #00a43f !important;
}
.fenpei {
color: #3471ff !important;
}
.detil {
color: #00a43f !important;
}
.edit {
color: #008477 !important;
}
.delete {
color: #f00 !important;
}
.follow-action {
display: inline-flex;
align-items: center;
padding: 4px;
color: #409eff;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
&:hover {
background: #f5f7fa;
}
}
.detail-info-table {
padding-left: 25px;
.row {
display: flex;
justify-content: flex-start;
margin-bottom: 6px;
> div {
flex: 1;
min-width: 0;
margin-right: 16px;
overflow: hidden;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.form-submit-row {
text-align: center;
}
.steer {
margin-left: 5px;
// display: inline-block;
// border: 1px solid #00a43f;
// padding: 0px 4px;
// color: #00a43f;
}
.dtitle {
line-height: 40px;
color: #333;
font-size: 16px;
font-weight: bold;
padding-left: 25px;
}
.detils-content {
padding-left: 25px;
img {
max-width: 100% !important;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
}
}
</style>
+215
View File
@@ -0,0 +1,215 @@
<template>
<div class="page-content">
<div v-if="!isMobile">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@company_add')">新增</ElButton>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
>
<template #default>
<ElTableColumn label="ID" prop="id" />
<ElTableColumn label="公司名称" prop="name" />
<ElTableColumn label="公司标识" prop="abbr" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'">
{{ scope.row.status ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('system@company_update')"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('system@company_destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
width="40%"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
<ElFormItem label="公司名称" prop="name">
<ElInput v-model="form.name" />
</ElFormItem>
<ElFormItem label="公司标识" prop="abbr">
<ElInput v-model="form.abbr" />
</ElFormItem>
<!-- <ElFormItem label="飞书标识" prop="tenant_key">
<ElInput v-model="form.tenant_key" />
</ElFormItem> -->
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
<div v-else class="mobile-hide-tip">
<p>该页面暂不支持移动端访问</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
abbr: [{ required: true, message: '请输入公司简称', trigger: 'blur' }]
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
})
const form = reactive<any>({
name: '',
id: '',
abbr: '',
status: 1
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await systemService.getCompanyList({
current: currentPage,
size: pageSize
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const dialogType = ref('add')
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.name = row.name
form.id = row.id
form.status = row.status
form.abbr = row.abbr
// form.tenant_key = row.tenant_key
} else {
form.name = ''
form.id = ''
form.status = 1
form.abbr = ''
// form.tenant_key = ''
}
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await systemService.companyDelete({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
if (dialogType.value === 'add') {
const { message } = await systemService.companyAdd(form)
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
} else {
const { message } = await systemService.companyEdit(form)
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
}
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
</style>
+350
View File
@@ -0,0 +1,350 @@
<template>
<div class="page-content">
<div v-if="!isMobile">
<ElRow>
<!-- <ElCol :xs="24" :sm="12" :lg="6">
<ElInput placeholder="名称"></ElInput>
</ElCol> -->
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<!-- <ElButton v-ripple>搜索</ElButton> -->
<ElButton @click="showDialog('add')" v-if="hasAuth('system@feishu_app_add')" v-ripple
>新增</ElButton
>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
>
<template #default>
<ElTableColumn label="ID" prop="id" width="80" />
<ElTableColumn label="公司" prop="corp_id" width="140">
<template #default="scope">
{{ scope.row.corpInfo.name }}
</template>
</ElTableColumn>
<ElTableColumn label="系统" prop="system_id" width="140">
<template #default="scope">
{{ scope.row.systemInfo.name }}
</template>
</ElTableColumn>
<ElTableColumn label="名称" prop="name" width="160" />
<ElTableColumn label="应用ID" prop="app_id" width="180" show-overflow-tooltip />
<ElTableColumn label="基础域" prop="base_uri" width="220" show-overflow-tooltip />
<ElTableColumn label="授权地址" prop="r_url" width="260" show-overflow-tooltip />
<ElTableColumn label="授权范围" prop="scopes" width="240" show-overflow-tooltip />
<ElTableColumn label="携带参数" prop="state" width="140" show-overflow-tooltip />
<ElTableColumn label="排序" prop="sort" width="90" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'">
{{ scope.row.status ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" width="180" />
<ElTableColumn fixed="right" label="操作" width="180px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('system@feishu_app_update')"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('system@feishu_app_destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
align-center
>
<ElForm
ref="formRef"
:model="form"
:rules="rules"
style="padding: 5px 20px"
label-width="100px"
>
<ElFormItem label="公司" prop="corp_id">
<ElSelect v-model="form.corp_id" placeholder="请选择公司" filterable style="width: 100%">
<ElOption v-for="item in companyList" :key="item.id" :label="item.name" :value="item.id" />
</ElSelect>
</ElFormItem>
<ElFormItem label="系统" prop="system_id">
<ElSelect v-model="form.system_id" placeholder="请选择系统" filterable style="width: 100%">
<ElOption v-for="item in systemsList" :key="item.id" :label="item.name" :value="item.id" />
</ElSelect>
</ElFormItem>
<ElFormItem label="名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入名称" />
</ElFormItem>
<!-- <ElFormItem label="企业标识" prop="tenant_key">
<ElInput v-model="form.tenant_key" placeholder="请输入企业标识" />
</ElFormItem> -->
<ElFormItem label="应用ID" prop="app_id">
<ElInput v-model="form.app_id" placeholder="请输入应用ID" />
</ElFormItem>
<ElFormItem label="应用秘钥" prop="app_secret">
<ElInput v-model="form.app_secret" placeholder="请输入应用秘钥" show-password />
</ElFormItem>
<ElFormItem label="基础域" prop="base_uri">
<ElInput v-model="form.base_uri" placeholder="请输入基础域" />
</ElFormItem>
<ElFormItem label="授权地址" prop="r_url">
<ElInput v-model="form.r_url" placeholder="请输入授权地址" />
</ElFormItem>
<ElFormItem label="授权范围" prop="scopes">
<ElInput v-model="form.scopes" type="textarea" :rows="3" placeholder="请输入授权范围" />
</ElFormItem>
<ElFormItem label="携带参数" prop="state">
<ElInput v-model="form.state" placeholder="请输入携带参数" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" placeholder="请输入排序" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
<div v-else class="mobile-hide-tip">
<p>该页面暂不支持移动端访问</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { systemService } from '@/api/systemApi'
import { UserService } from '@/api/usersApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({
corp_id: [{ required: true, message: '请选择公司', trigger: 'change' }],
system_id: [{ required: true, message: '请选择系统', trigger: 'change' }],
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
// app_id: [{ required: true, message: '请输入应用ID', trigger: 'blur' }],
// app_secret: [{ required: true, message: '请输入应用秘钥', trigger: 'blur' }],
// base_uri: [{ required: true, message: '请输入基础域', trigger: 'blur' }],
// r_url: [{ required: true, message: '请输入授权地址', trigger: 'blur' }],
// scopes: [{ required: true, message: '请输入授权范围', trigger: 'blur' }],
// state: [{ required: true, message: '请输入携带参数', trigger: 'blur' }],
// sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
// status: [{ required: true, message: '请选择状态', trigger: 'change' }]
})
const form = reactive({
corp_id: '',
system_id: '',
name: '',
// tenant_key: '',
app_id: '',
app_secret: '',
base_uri: '',
r_url: '',
scopes: '',
state: '',
sort: 0,
status: 1,
id: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const companyList = ref<any[]>([])
const systemsList = ref<any[]>([])
const getcompanyList = async () => {
const response = await UserService.getCompanyList()
const list = (response as any)?.data ?? (response as any)?.records ?? response
companyList.value = Array.isArray(list) ? list : []
}
const getsystemsList = async () => {
const response = await UserService.getsystemsList()
const list = (response as any)?.data ?? (response as any)?.records ?? response
systemsList.value = Array.isArray(list) ? list : []
}
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
getcompanyList()
getsystemsList()
})
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await systemService.getFeishuAppList({
current: currentPage,
size: pageSize
})
tableData.value = records.map((item: any, index: number) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const dialogType = ref('add')
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式,填充所有字段
Object.assign(form, {
corp_id: row.corp_id || '',
system_id: row.system_id || '',
name: row.name || '',
// tenant_key: row.tenant_key || '',
app_id: row.app_id || '',
app_secret: row.app_secret || '',
base_uri: row.base_uri || '',
r_url: row.r_url || '',
scopes: row.scopes || '',
state: row.state || '',
sort: row.sort || 0,
status: row.status !== undefined ? row.status : 1,
id: row.id || ''
})
} else {
// 新增模式,重置所有字段
Object.assign(form, {
corp_id: '',
system_id: '',
name: '',
// tenant_key: '',
app_id: '',
app_secret: '',
base_uri: '',
r_url: '',
scopes: '',
state: '',
sort: 0,
status: 1,
id: ''
})
}
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await systemService.feishuAppDelete({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
if (dialogType.value === 'add') {
const { message } = await systemService.feishuAppAdd({
corp_id: form.corp_id,
system_id: form.system_id,
name: form.name,
app_id: form.app_id,
app_secret: form.app_secret,
base_uri: form.base_uri,
r_url: form.r_url,
scopes: form.scopes,
state: form.state,
sort: form.sort,
status: form.status
})
ElMessage.success(message)
} else {
const { message } = await systemService.feishuAppEdit({
id: form.id,
corp_id: form.corp_id,
system_id: form.system_id,
name: form.name,
app_id: form.app_id,
app_secret: form.app_secret,
base_uri: form.base_uri,
r_url: form.r_url,
scopes: form.scopes,
state: form.state,
sort: form.sort,
status: form.status
})
ElMessage.success(message)
}
dialogVisible.value = false
getWorkList()
}
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
</style>
+751
View File
@@ -0,0 +1,751 @@
<template>
<ArtTableFullScreen>
<div class="menu-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
<template #left>
<el-segmented
v-model="yearVal"
size="large"
:options="segmentedOptions"
@change="searchArticleByYear"
>
<!-- <template #default="scope">
<div class="flex flex-col items-center gap-2 p-2">
<div>{{ scope.item.label }}</div>
</div>
</template> -->
</el-segmented>
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
<ElButton
v-auth="'system@menu_add'"
@click="showModel('menu', null)"
v-ripple
style="margin-left: 20px"
>
添加菜单
</ElButton>
<ElButton @click="toggleExpand" v-ripple>
{{ isExpanded ? '收起' : '展开' }}
</ElButton>
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton> -->
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
rowKey="path"
ref="tableRef"
:loading="loading"
:data="filteredTableData"
:marginTop="10"
:stripe="false"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="资源类型" prop="type">
<el-select v-model="form.type">
<el-option label="目录" value="directory" />
<el-option label="菜单" value="menu" />
<el-option label="权限" value="permission" />
</el-select>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="资源名称" prop="name">
<ElInput v-model="form.name" placeholder="资源名称"></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限标识" prop="code">
<ElInput v-model="form.code" placeholder="权限标识"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="图标" prop="icon">
<ArtIconSelector v-model="form.icon" :iconType="iconType" width="100%" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="12" v-if="form.type !== 'permission'">
<ElFormItem label="路由地址" prop="url">
<ElInput v-model="form.url" placeholder="路由地址"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="打开方式" prop="open_type">
<ElSelect v-model="form.open_type" class="account-select">
<el-option label="内部" value="internal" />
<el-option label="外部" value="external" />
<el-option label="嵌入" value="iframe" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="12">
<ElFormItem label="资源排序" prop="sort" style="width: 100%">
<ElInputNumber
v-model="form.sort"
style="width: 100%"
@change="handleChange"
:min="1"
controls-position="right"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="外部链接" prop="link">
<ElInput
v-model="form.link"
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="系统" prop="system_id">
<ElSelect v-model="form.system_id" class="account-select">
<ElOption
v-for="account in systemsList"
:key="account.id"
:label="account.name"
:value="account.id"
>
<span>{{ account.name }}</span>
</ElOption>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="父级" prop="parent_id">
<el-tree-select
v-model="form.parent_id"
:data="parentList"
:props="{
value: 'id',
label: 'name',
children: 'children'
}"
check-strictly
:render-after-expand="false"
style="width: 240px"
/>
<!-- <ElSelect v-model="form.parent_id" class="account-select">
<ElOption
v-for="account in parentList"
:key="account.id"
:label="account.name"
:value="account.id"
>
<span>{{ account.name }}</span>
</ElOption>
</ElSelect> -->
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="5">
<ElFormItem label="是否启用" prop="status">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="页面缓存" prop="keep_alive">
<ElSwitch
v-model="form.keep_alive"
:active-value="1"
:inactive-value="0"
></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否隐藏" prop="is_hide">
<ElSwitch v-model="form.is_hide" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="标签页是否隐藏" prop="is_hide_tab" label-width="120px">
<ElSwitch
v-model="form.is_hide_tab"
:active-value="1"
:inactive-value="0"
></ElSwitch>
</ElFormItem>
</ElCol>
<!-- <ElCol :span="5">
<ElFormItem label="是否内嵌" prop="isMenu">
<ElSwitch v-model="form.isIframe"></ElSwitch>
</ElFormItem>
</ElCol> -->
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="5">
<ElFormItem label="是否首页" prop="is_home">
<ElSwitch v-model="form.is_home" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="10">
<ElFormItem label="不需要认证" prop="no_auth" label-width="120px">
<ElSwitch v-model="form.no_auth" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
<template #footer>
<span class="dialog-footer">
<ElButton @click="dialogVisible = false">取 消</ElButton>
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
</span>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { IconTypeEnum } from '@/enums/appEnum'
import { formatMenuTitle } from '@/router/utils/utils'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { ElPopover, ElButton } from 'element-plus'
import { AppRouteRecord } from '@/types/router'
import { useAuth } from '@/composables/useAuth'
import { SearchFormItem } from '@/types'
import { systemService } from '@/api/systemApi'
import { UserService } from '@/api/usersApi'
const yearVal = ref<any>('')
defineOptions({ name: 'Menus' })
const { hasAuth } = useAuth()
// const { menuList } = storeToRefs(useMenuStore())
const menuList = ref<any[]>([])
const loading = ref(false)
const systemsListtab = ref<any>([])
const segmentedOptions = computed(() =>
Array.isArray(systemsListtab.value)
? systemsListtab.value.map((item: any) => ({ label: item.name, value: item.id }))
: []
)
const getsystemsListtab = async () => {
const response = await UserService.getsystemsList()
const list = (response as any)?.data?.records ?? (response as any)?.records ?? response ?? []
systemsListtab.value = Array.isArray(list) ? list : []
if (!yearVal.value && systemsListtab.value.length > 0) {
yearVal.value = systemsListtab.value[0].id
}
}
//系统
const systemsList = ref<any>()
const getsystemsList = async () => {
const response = await UserService.getsystemsList()
systemsList.value = response || []
}
//父级
const parentList = ref<any>()
const getparentList = async () => {
const response = await systemService.getMenuList()
parentList.value = response || []
}
// 定义表单搜索初始值
const initialSearchState = {
name: '',
route: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
// 增加实际应用的搜索条件状态
const appliedFilters = reactive({ ...initialSearchState })
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
Object.assign(appliedFilters, { ...initialSearchState })
getTableData()
}
// 搜索处理
const handleSearch = () => {
// 将当前输入的筛选条件应用到实际搜索
Object.assign(appliedFilters, { ...formFilters })
getTableData()
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '菜单名称',
prop: 'name',
type: 'input',
config: {
clearable: true
}
},
{
label: '路由地址',
prop: 'route',
type: 'input',
config: {
clearable: true
}
}
]
// 构建菜单类型标签
const buildMenuTypeTag = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return 'info'
} else if (row.meta?.link && row.meta?.isIframe) {
return 'success'
} else if (row.path) {
return 'primary'
} else if (row.meta?.link) {
return 'warning'
}
}
// 构建菜单类型文本
const buildMenuTypeText = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return '目录'
} else if (row.meta?.link && row.meta?.isIframe) {
return '内嵌'
} else if (row.path) {
return '菜单'
} else if (row.meta?.link) {
return '外链'
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'meta.title',
label: '菜单名称',
minWidth: 120,
formatter: (row: AppRouteRecord) => {
return formatMenuTitle(row.meta?.title)
}
},
{
prop: 'type',
label: '菜单类型',
formatter: (row: AppRouteRecord) => {
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
}
},
{
prop: 'path',
label: '路由',
formatter: (row: AppRouteRecord) => {
return row.meta?.link || row.path || ''
}
},
{
prop: 'meta.authList',
label: '可操作权限',
formatter: (row: AppRouteRecord) => {
return h(
'div',
{},
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
return h(
ElPopover,
{
placement: 'top-start',
title: '操作',
width: 200,
trigger: 'click',
key: index
},
{
default: () =>
h('div', { style: 'margin: 0; text-align: right' }, [
h(
ElButton,
{
size: 'small',
type: 'primary',
onClick: () => showModel('button', item, false, row)
},
{ default: () => '编辑' }
),
h(
ElButton,
{
size: 'small',
type: 'danger',
onClick: () => deleteAuth(item)
},
{ default: () => '删除' }
)
]),
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
}
)
})
)
}
},
{
prop: 'date',
label: '编辑时间',
formatter: () => '2022-3-12 12:00:00'
},
{
prop: 'status',
label: '隐藏菜单',
formatter: (row) => {
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
row.meta.isHide ? '是' : '否'
)
}
},
{
prop: 'operation',
label: '操作',
width: 180,
formatter: (row: AppRouteRecord) => {
return h('div', [
// 后端模式权限标识
hasAuth('system@menu_add') &&
h(ArtButtonTable, {
type: 'add',
onClick: () => showModel('menu', row, true)
}),
hasAuth('system@menu_update') &&
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
hasAuth('system@menu_destroy') &&
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteMenu(row)
})
])
}
}
])
const handleRefresh = () => {
getTableData()
}
const dialogVisible = ref(false)
const form = reactive({
// 菜单
system_id: '',
type: 'menu', // 资源类型
parent_id: '',
name: '',
url: '',
code: '',
icon: '',
status: 1,
sort: 1,
is_home: 0,
keep_alive: 1,
is_hide: 0,
is_hide_tab: 0,
no_auth: 0,
link: '',
open_type: 'internal'
})
const iconType = ref(IconTypeEnum.UNICODE)
// const labelPosition = ref('menu')
const rules = reactive<FormRules>({
name: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
system_id: [{ required: true, message: '请选择系统', trigger: 'blur' }],
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
})
const tableData = ref<AppRouteRecord[]>([])
onMounted(() => {
getsystemsListtab()
getTableData()
})
const searchArticleByYear = () => {
getTableData()
}
// const handleSegmentedClick = (item: any) => {
// // 若点击的是当前项也强制刷新
// yearVal.value = item?.value ?? yearVal.value
// getTableData()
// }
const getTableData = async () => {
loading.value = true
// 清空旧数据,避免视觉上出现追加
menuList.value = []
tableData.value = []
// 并发安全:本次请求标识
requestCounter += 1
const reqId = requestCounter
const params: any = {
system_id: yearVal.value || '1'
}
try {
const res = await systemService.getMenutreeList(params)
// 若在等待过程中又发起了新请求,则丢弃本次结果
if (reqId !== requestCounter) return
menuList.value = Array.isArray(res) ? res : []
} catch (e) {
console.error(e)
menuList.value = []
}
// console.log(res)
// menuList.value = data
// 基于 appliedFilters 在这里进行过滤,并将结果赋值给 tableData
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
const filterMenus = (items: AppRouteRecord[]): AppRouteRecord[] => {
return items.reduce((acc: AppRouteRecord[], item: AppRouteRecord) => {
// 浅拷贝节点,避免直接修改原数据
const copy: AppRouteRecord = { ...item }
if (copy.children && copy.children.length > 0) {
copy.children = filterMenus(copy.children)
}
const menuTitle = formatMenuTitle(copy.meta?.title || '').toLowerCase()
const menuPath = (copy.path || '').toLowerCase()
const nameMatch = !searchName || menuTitle.includes(searchName)
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
const hasChildren = Array.isArray(copy.children) && copy.children.length > 0
if (hasChildren || (nameMatch && routeMatch)) {
acc.push(copy)
}
return acc
}, [])
}
tableData.value = filterMenus(menuList.value)
loading.value = false
}
// 过滤后的表格数据改为来源于 getTableData 的结果
const filteredTableData = computed(() => tableData.value)
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const dialogTitle = computed(() => {
// const type = labelPosition.value === 'menu' ? '菜单' : '权限'
return isEdit.value ? `编辑资源` : `新建资源`
})
const showDialog = (type: string, row: AppRouteRecord) => {
showModel('menu', row)
}
const handleChange = () => {}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
if (isEdit.value) {
const payload = { ...form, id: itemid.value }
await systemService.menuEdit(payload as any)
} else {
await systemService.menuAdd(form)
}
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
dialogVisible.value = false
// 刷新表格数据
getTableData()
} catch {
ElMessage.error(`${isEdit.value ? '编辑' : '新增'}失败`)
}
}
})
}
const itemid = ref('')
const showModel = (type: string, row?: any, padd: boolean = false, data?: any) => {
getsystemsList()
getparentList()
dialogVisible.value = true
// labelPosition.value = type
isEdit.value = false
// lockMenuType.value = lock
resetForm()
itemid.value = ''
if (data) {
isEdit.value = true
form.type = 'permission'
form.system_id = data.meta.systemId
form.parent_id = data.id
form.name = row.title
form.code = row.authMark
itemid.value = row.id
} else if (padd) {
nextTick(() => {
form.parent_id = row.id
form.system_id = row.meta.systemId
})
} else if (row && !padd) {
isEdit.value = true
nextTick(() => {
// 回显数据
itemid.value = row.id
form.system_id = row.meta.systemId
form.parent_id = row.meta.parentId == 0 ? '' : row.meta.parentId
form.name = row.name
form.type = row.meta.type
form.url = row.path
form.code = row.meta.code
form.icon = row.meta.icon
form.sort = row.meta.sort
form.is_home = row.meta.isHome
form.keep_alive = row.meta.keepAlive
form.is_hide = row.meta.isHide
form.is_hide_tab = row.meta.isHideTab
form.no_auth = row.meta.noAuth
form.status = row.meta.status
form.link = row.meta.link
form.open_type = row.meta.openType
})
}
}
const resetForm = () => {
formRef.value?.resetFields()
Object.assign(form, {
// 菜单
system_id: '',
type: 'menu', // 资源类型
parent_id: '',
name: '',
url: '',
code: '',
icon: '',
status: 1,
sort: 1,
is_home: 0,
keep_alive: 1,
is_hide: 0,
is_hide_tab: 0,
no_auth: 0,
link: '',
open_type: 'internal'
})
}
const deleteMenu = async (item: any) => {
try {
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await systemService.menuDelete({ id: item.id })
ElMessage.success(res?.message || '删除成功!')
// 刷新表格数据
getTableData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
const deleteAuth = async (item: any) => {
try {
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await systemService.menuDelete({ id: item.id })
ElMessage.success(res?.message || '删除成功!')
// 刷新表格数据
getTableData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 添加一个控制变量
// const lockMenuType = ref(false)
const isExpanded = ref(false)
const tableRef = ref()
// 数据请求并发保护计数器
let requestCounter = 0
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
nextTick(() => {
if (tableRef.value) {
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
}
})
}
</script>
<style lang="scss" scoped>
.menu-page {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
:deep(.small-btn) {
height: 22px !important;
padding: 0 5px !important;
font-size: 10px !important;
margin-bottom: 5px;
}
:deep(.el-dialog__footer) {
z-index: inherit;
.el-button > span {
z-index: inherit;
}
}
}
</style>
+446
View File
@@ -0,0 +1,446 @@
<template>
<div class="page-content">
<ElRow>
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput placeholder="角色名称"></ElInput>
</ElCol>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple>搜索</ElButton>
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@role_add')">新增角色</ElButton>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="roleList"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="角色名称" prop="name" />
<ElTableColumn label="描述" prop="desc" />
<ElTableColumn label="启用" prop="enable">
<template #default="scope">
<ElTag :type="scope.row.status == 1 ? 'primary' : 'info'">
{{ scope.row.status == 1 ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at"></ElTableColumn>
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showPermissionDialog(scope.row)"
v-if="hasAuth('system@role_perm')"
>
菜单权限
</el-button>
<el-button
type="success"
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('system@role_update')"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('system@role_destroy')"
>删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
width="30%"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="角色名称" prop="name">
<ElInput v-model="form.name" />
</ElFormItem>
<ElFormItem label="描述" prop="desc">
<ElInput v-model="form.desc" type="textarea" :rows="3" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInput type="number" v-model="form.sort" />
</ElFormItem>
<ElFormItem label="启用">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="permissionDialog"
title="菜单权限"
width="520px"
align-center
class="el-dialog-border"
>
<ElScrollbar height="70vh">
<ElTree
ref="treeRef"
:data="processedMenuList"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="handleTreeCheck"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</ElScrollbar>
<template #footer>
<div class="dialog-footer">
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
isSelectAll ? '取消全选' : '全部选择'
}}</ElButton>
<ElButton type="primary" @click="savePermission">保存</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { formatMenuTitle } from '@/router/utils/utils'
import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
// import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
const loading = ref(false)
defineOptions({ name: 'Role' })
const dialogVisible = ref(false)
const permissionDialog = ref(false)
// 权限树数据(来自接口 getPermissionMenuList)
const permissionMenuList = ref<any[]>([])
const treeRef = ref()
const isExpandAll = ref(true)
const isSelectAll = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const formRef = ref<FormInstance>()
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
})
const form = reactive<any>({
id: '',
name: '',
desc: '',
sort: 0,
status: 1
})
const roleList = ref<any[]>([])
onMounted(() => {
getTableData()
})
// const getTableData = () => {
// roleList.value = ROLE_LIST_DATA
// }
// 获取列表数据
const getTableData = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await systemService.getRoleList({
current: currentPage,
size: pageSize
})
roleList.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getTableData()
}
const dialogType = ref('add')
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.id = row.id
form.name = row.name
form.desc = row.desc
form.sort = row.sort
form.status = row.status
} else {
form.name = ''
form.desc = ''
form.sort = 0
form.status = 1
}
}
const roleId = ref('')
const showPermissionDialog = async (row: any) => {
permissionDialog.value = true
roleId.value = row.id
try {
const res = await systemService.getPermissionMenuList({ role_id: row.id })
// 兼容不同返回结构(data/records/直接数组)
const list = (res as any)?.data ?? (res as any)?.records ?? (res as any)
permissionMenuList.value = Array.isArray(list) ? list : []
// 根据接口返回的 selected 字段设置默认选中
nextTick(() => {
const keys = buildDefaultCheckedKeys(permissionMenuList.value)
treeRef.value?.setCheckedKeys(keys)
handleTreeCheck()
})
} catch (e) {
permissionMenuList.value = []
}
}
// 构建默认选中的 key 列表(与 node-key="id" 一致)
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
const keys: Array<string | number> = []
const walk = (node: any) => {
if (node?.meta?.selected) {
keys.push(node.id)
}
if (Array.isArray(node?.meta?.authList)) {
node.meta.authList.forEach((auth: any) => {
if (auth?.selected) {
keys.push(`${node.id}__auth__${auth.authMark}`)
}
})
}
if (Array.isArray(node?.children)) node.children.forEach(walk)
}
source.forEach(walk)
return keys
}
// 处理菜单数据,将 authList 转换为子节点
const processedMenuList = computed(() => {
const processNode = (node: any) => {
// 规范化 id,确保树节点唯一键与 node-key 对应
const processed: any = { ...node, id: node.id }
// 如果有 authList,将其转换为子节点
if (node.meta && node.meta.authList && node.meta.authList.length) {
const authNodes = node.meta.authList.map((auth: any) => ({
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
id: `${node.id}__auth__${auth.authMark}`,
name: `${node.name}_${auth.authMark}`,
label: auth.title,
auth_mark: auth.authMark,
auth_id: auth.id,
isAuth: true,
checked: auth.selected || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
// 递归处理子节点
if (processed.children) {
processed.children = processed.children.map(processNode)
}
return processed
}
const source = permissionMenuList.value || []
return source.map(processNode)
})
const defaultProps = {
children: 'children',
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
}
const deleteRole = async (row: any) => {
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const res = await systemService.roleDelete({ id: row.id })
ElMessage.success(res?.message || '删除成功!')
getTableData()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
if (dialogType.value === 'add') {
// 新增角色逻辑
const res = await systemService.roleAdd(form)
ElMessage.success(res?.message || '提交成功!')
getTableData()
} else {
// 编辑角色逻辑
const res = await systemService.roleEdit(form)
ElMessage.success(res?.message || '提交成功!')
getTableData()
}
dialogVisible.value = false
formEl.resetFields()
}
const getSelectedIds = () => {
const tree = treeRef.value
if (!tree) return { menuIds: [], authIds: [] }
const checkedNodes = tree.getCheckedNodes?.(false) || []
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
const allNodes = [...checkedNodes, ...halfCheckedNodes]
const menuIds = Array.from(
new Set(
allNodes
.filter((n: any) => !n.isAuth && n.id !== undefined && n.id !== null)
.map((n: any) => n.id)
)
)
const authIds = Array.from(
new Set(
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
)
)
const ids = Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
return { menuIds, authIds, ids }
}
const savePermission = async () => {
const { ids } = getSelectedIds()
// console.log('选中的菜单ID:', menuIds)
// console.log('选中的权限ID:', authIds)
// console.log('合并后的全部ID:', ids)
const res = await systemService.roleAuth({
role_id: roleId.value,
resources: ids
})
ElMessage.success(res?.message || '权限保存成功')
permissionDialog.value = false
}
const toggleExpandAll = () => {
const tree = treeRef.value
if (!tree) return
// 使用store.nodesMap直接控制所有节点的展开状态
const nodes = tree.store.nodesMap
for (const node in nodes) {
nodes[node].expanded = !isExpandAll.value
}
isExpandAll.value = !isExpandAll.value
}
const toggleSelectAll = () => {
const tree = treeRef.value
if (!tree) return
if (!isSelectAll.value) {
// 全选:获取所有节点的key并设置为选中
const allKeys = getAllNodeKeys(processedMenuList.value)
tree.setCheckedKeys(allKeys)
} else {
// 取消全选:清空所有选中
tree.setCheckedKeys([])
}
isSelectAll.value = !isSelectAll.value
}
const getAllNodeKeys = (nodes: any[]): Array<string | number> => {
const keys: Array<string | number> = []
const traverse = (nodeList: any[]) => {
nodeList.forEach((node) => {
if (node.id !== undefined && node.id !== null) {
keys.push(node.id)
}
if (node.children && node.children.length > 0) {
traverse(node.children)
}
})
}
traverse(nodes)
return keys
}
const handleTreeCheck = () => {
const tree = treeRef.value
if (!tree) return
// 使用树组件的getCheckedKeys方法获取选中的节点
const checkedKeys = tree.getCheckedKeys()
const allKeys = getAllNodeKeys(processedMenuList.value)
// 判断是否全选:选中的节点数量等于总节点数量
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
}
</script>
<style lang="scss" scoped>
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
</style>
+471
View File
@@ -0,0 +1,471 @@
<template>
<div class="page-content">
<div v-if="!isMobile">
<ElRow>
<!-- <ElCol :xs="24" :sm="12" :lg="6">
<ElInput placeholder="名称"></ElInput>
</ElCol> -->
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<!-- <ElButton v-ripple>搜索</ElButton> -->
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@systems_add')">新增</ElButton>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
>
<template #default>
<ElTableColumn label="ID" prop="id" width="80" />
<ElTableColumn label="系统编码" prop="code" />
<ElTableColumn label="系统名称" prop="name" />
<ElTableColumn label="系统类型" prop="type">
<template #default="scope">
<ElTag :type="getTypeTagType(scope.row.type)">
{{ getTypeText(scope.row.type) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="系统描述" prop="desc" show-overflow-tooltip />
<ElTableColumn label="域名URL" prop="domain_url" show-overflow-tooltip />
<ElTableColumn label="区分角色" prop="is_role">
<template #default="scope">
<ElTag :type="scope.row.is_role ? 'success' : 'info'">
{{ scope.row.is_role ? '是' : '否' }}
</ElTag>
</template>
</ElTableColumn>
<!-- <ElTableColumn label="排序" prop="sort" width="80" /> -->
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'">
{{ scope.row.status ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('system@systems_update')"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('system@systems_destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
width="60%"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
<ElFormItem label="系统编码" prop="code">
<ElInput v-model="form.code" placeholder="请输入系统编码" />
</ElFormItem>
<ElFormItem label="系统名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入系统名称" />
</ElFormItem>
<ElFormItem label="系统类型" prop="type">
<ElSelect v-model="form.type" placeholder="请选择系统类型" style="width: 100%">
<ElOption label="管理后台" value="admin" />
<ElOption label="Web端" value="web" />
<ElOption label="小程序" value="miniapp" />
<ElOption label="工具" value="tool" />
<ElOption label="其他" value="other" />
</ElSelect>
</ElFormItem>
<ElFormItem label="系统描述" prop="desc">
<ElInput v-model="form.desc" type="textarea" :rows="3" placeholder="请输入系统描述" />
</ElFormItem>
<ElFormItem label="系统图标" prop="icon">
<ElUpload
v-model:file-list="iconFileList"
:show-file-list="false"
:headers="uploadHeaders"
:action="actionImageUplode"
:before-upload="beforeIconUpload"
:on-success="handleIconSuccess"
:on-error="handleIconError"
accept="image/*"
class="icon-uploader"
>
<div v-if="form.icon" class="icon-preview">
<img :src="file_url" alt="系统图标" class="icon-image" />
<div class="icon-overlay">
<el-icon><Plus /></el-icon>
</div>
</div>
<el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
</ElUpload>
</ElFormItem>
<ElFormItem label="域名URL" prop="domain_url">
<ElInput v-model="form.domain_url" placeholder="请输入域名URL" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" placeholder="请输入排序" />
</ElFormItem>
<ElFormItem label="是否区分角色权限" prop="is_role">
<ElSwitch v-model="form.is_role" :active-value="1" :inactive-value="0" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
<div v-else class="mobile-hide-tip">
<p>该页面暂不支持移动端访问</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' })
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const iconFileList = ref<any[]>([])
const typeList = ref<any[]>([])
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
const rules = reactive<FormRules>({
code: [{ required: true, message: '请输入系统编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入系统名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择系统类型', trigger: 'change' }]
// desc: [
// { required: true, message: '请输入系统描述', trigger: 'blur' }
// ],
// icon: [
// { required: true, message: '请输入系统图标', trigger: 'blur' }
// ],
// domain_url: [
// { required: true, message: '请输入域名URL', trigger: 'blur' }
// ],
// is_role: [
// { required: true, message: '请选择是否区分角色权限', trigger: 'change' }
// ],
// sort: [
// { required: true, message: '请输入排序', trigger: 'blur' }
// ],
// status: [
// { required: true, message: '请选择状态', trigger: 'change' }
// ]
})
const file_url = ref('')
const form = reactive({
code: '',
name: '',
type: '',
desc: '',
icon: '',
domain_url: '',
is_role: false,
sort: 0,
status: true,
id: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await systemService.getSystemList({
current: currentPage,
size: pageSize
})
tableData.value = records.map((item: any, index: number) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
// 系统类型相关辅助函数
const getTypeText = (type: string) => {
const typeMap: { [key: string]: string } = {
'admin': '管理后台',
'web': 'Web端',
'miniapp': '小程序',
'tool': '工具',
'other': '其他'
}
return typeMap[type] || type
}
const getTypeTagType = (type: string) => {
const typeMap: { [key: string]: 'primary' | 'success' | 'warning' | 'info' | 'danger' } = {
'admin': 'danger',
'web': 'primary',
'miniapp': 'success',
'tool': 'warning',
'other': 'info'
}
return typeMap[type] || 'info'
}
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type as 'add' | 'edit'
if (type === 'edit' && row) {
file_url.value = row.icon
// 编辑模式,填充所有字段
Object.assign(form, {
code: row.code || '',
name: row.name || '',
type: row.type || 'web',
desc: row.desc || '',
icon: row.icon || row.icon_url || '', // 优先使用 icon,其次 icon_url
domain_url: row.domain_url || '',
is_role: row.is_role !== undefined ? row.is_role : 0,
sort: row.sort || 0,
status: row.status !== undefined ? row.status : 1,
id: row.id || '' // 编辑时填充文件名,如果没有则使用 icon
})
} else {
file_url.value = ''
// 新增模式,重置所有字段
Object.assign(form, {
code: '',
name: '',
type: 'web',
desc: '',
icon: '',
domain_url: '',
is_role: 0,
sort: 0,
status: 1,
id: ''
})
}
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await systemService.systemDelete({
id: row.id,
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
if (dialogType.value === 'add') {
const { message } = await systemService.systemAdd({
code: form.code,
name: form.name,
type: form.type,
desc: form.desc,
icon: form.icon,
domain_url: form.domain_url,
is_role: form.is_role,
sort: form.sort,
status: form.status
})
ElMessage.success(message)
} else {
const { message } = await systemService.systemEdit({
id: form.id,
code: form.code,
name: form.name,
type: form.type,
desc: form.desc,
icon: form.icon,
domain_url: form.domain_url,
is_role: form.is_role,
sort: form.sort,
status: form.status
})
ElMessage.success(message)
}
dialogVisible.value = false
getWorkList()
}
})
}
// 图标上传相关函数
const beforeIconUpload = (file: File) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
const handleIconSuccess = (response: any, file: any) => {
// 接口返回 file_url 用于回显,file_name 用于传参
if (response.data) {
form.icon = response.data.file_url || response.data.file_name // 回显用 file_url
file_url.value = response.data.file_url
} else {
form.icon = response.file_url || response.file_name
file_url.value = response.file_url
}
ElMessage.success('图标上传成功')
}
const handleIconError = () => {
ElMessage.error('图标上传失败')
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.el-table {
margin-top: 20px;
}
.icon-uploader {
text-align: center;
}
.icon-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 64px;
height: 64px;
line-height: 64px;
text-align: center;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
transition: border-color 0.3s;
}
.icon-uploader-icon:hover {
border-color: #409eff;
}
.icon-preview {
position: relative;
width: 64px;
height: 64px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
overflow: hidden;
}
.icon-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.icon-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
}
.icon-overlay .el-icon {
color: white;
font-size: 20px;
}
.icon-preview:hover .icon-overlay {
opacity: 1;
}
.icon-tip {
font-size: 12px;
color: #999;
margin-top: 8px;
text-align: center;
}
</style>
+860
View File
@@ -0,0 +1,860 @@
<template>
<div class="customer-page">
<!-- 顶部操作栏 -->
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
<el-form-item label="状态:" label-width="50px">
<el-select v-model="selectValue" style="min-width: 120px">
<el-option label="全部" value="" />
<el-option label="正常" value="1" />
<el-option label="离职" value="2" />
</el-select>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
lass="customer-table"
>
<template #default>
<!-- <el-table-column prop="id" label="用户ID" align="center" /> -->
<el-table-column prop="sn" label="SN" align="center" />
<el-table-column prop="follow" label="头像" align="center">
<template #default="scope">
<img
:src="scope.row.avatar"
alt="头像"
class="avatar"
@click="handleAvatarClick(scope.row.avatar)"
style="cursor: pointer"
/>
</template>
</el-table-column>
<el-table-column prop="name" label="名字" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="corp_name" label="所属公司" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.status)">
{{ scope.row.status_txt }}
</span>
</template>
</el-table-column>
<el-table-column prop="customer_count" label="客户数量" align="center" />
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
<el-link
type="success"
underline="never"
style="margin-right: 8px"
@click="showPermissionDialog(scope.row)"
v-if="hasAuth('user@direct_perm')"
>
<i class="iconfont-sys">&#xe723;</i>直接授权
</el-link>
<el-link
type="warning"
underline="never"
style="margin-right: 8px"
@click="showroleDialog(scope.row)"
v-if="hasAuth('user@role_system')"
>
<i class="iconfont-sys">&#xe830;</i>角色&系统
</el-link>
<el-link
type="primary"
underline="never"
style="margin-right: 8px"
@click="gotransfer(scope.row)"
v-if="hasAuth('user@customer_transfer')"
>
<i class="iconfont-sys"> &#xe794;</i>客户转移
</el-link>
<el-link
type="success"
underline="never"
style="margin-right: 8px"
@click="onEdit(scope.row)"
v-if="hasAuth('user@edit')"
>
<i class="iconfont-sys"> &#xe6bf;</i>编辑
</el-link>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
</div>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="详情" width="75%">
<div>
<el-button type="primary" @click="plzyDialogVisiblefn">
<i class="iconfont-sys">&#xe794;</i> 批量转移
</el-button>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loadingkh"
:data="tableDatakh"
:currentPage="paginationkh.currentPage"
:pageSize="paginationkh.pageSize"
:total="paginationkh.total"
@selection-change="handleSelectionChange"
@size-change="handleSizeChangekh"
@current-change="handleCurrentChangekh"
lass="customer-table"
>
<template #default>
<el-table-column type="selection" width="55px" align="center"></el-table-column>
<el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
<el-table-column prop="channel_name" label="渠道" align="center" />
<el-table-column prop="country_name" label="国家" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.follow)">
{{ scope.row.follow_txt }}
</span>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
</el-dialog>
<!-- 批量转移对话框 -->
<el-dialog v-model="plzyDialogVisible" title="批量转移" width="360px">
<el-form :model="plzyForm" label-width="0">
<el-form-item>
<el-select
v-model="plzyForm.user_id"
filterable
placeholder="请选择转移人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="plzyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
<el-dialog
title="编辑用户"
v-model="AddDialogVisible"
width="800px"
:close-on-click-modal="false"
>
<el-form :model="addForm" ref="addFormRef" label-width="80px" class="add-form">
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="用户名" prop="country">
<el-input v-model="addForm.name" placeholder="请输入用户名" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="Email" prop="email">
<el-input v-model="addForm.email" placeholder="请输入Email" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="电话" prop="tel">
<el-input v-model="addForm.phone" placeholder="请输入电话" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="上级" prop="report_to">
<el-select
v-model="addForm.report_to"
filterable
placeholder="请选择上级"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="状态" prop="status">
<el-select v-model="addForm.status">
<el-option label="正常" :value="1" />
<el-option label="离职" :value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<div class="form-submit-row">
<el-button type="primary" size="large" style="width: 50%" @click="handleAddSubmit"
>提交</el-button
>
</div>
</el-form>
</el-dialog>
<!-- 图片预览组件 -->
<ElImageViewer
v-if="showImageViewer"
:url-list="[currentImageUrl]"
:hide-on-click-modal="true"
@close="showImageViewer = false"
/>
<ElDialog
v-model="permissionDialog"
title="菜单权限"
width="520px"
align-center
class="el-dialog-border"
>
<ElScrollbar height="70vh">
<ElTree
ref="treeRef"
:data="processedMenuList"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="handleTreeCheck"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</ElScrollbar>
<template #footer>
<div class="dialog-footer">
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
isSelectAll ? '取消全选' : '全部选择'
}}</ElButton>
<ElButton type="primary" @click="savePermission">保存</ElButton>
</div>
</template>
</ElDialog>
<el-dialog
title="角色&系统设置"
v-model="roleDialogVisible"
width="700px"
:close-on-click-modal="false"
>
<el-form :model="roleForm" ref="addFormRef" label-width="80px" class="add-form">
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="角色" prop="status">
<ElSelect
v-model="roleForm.role_ids"
multiple
class="account-select"
placeholder="请选择角色"
>
<ElOption
v-for="account in roleOptions"
:key="account.id"
:label="account.name"
:value="account.id"
>
<span>{{ account.name }}</span>
</ElOption>
</ElSelect>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="系统" prop="status">
<ElSelect
v-model="roleForm.system_ids"
multiple
class="account-select"
placeholder="请选择系统"
>
<ElOption
v-for="account in systemsList"
:key="account.id"
:label="account.name"
:value="account.id"
>
<span>{{ account.name }}</span>
</ElOption>
</ElSelect>
</el-form-item>
</el-col>
</el-row>
<div class="form-submit-row">
<el-button type="primary" size="large" style="width: 70%" @click="handleRoleSubmit"
>提交</el-button
>
</div>
</el-form>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { ElImageViewer } from 'element-plus'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { systemService } from '@/api/systemApi'
import { formatMenuTitle } from '@/router/utils/utils'
import { UserService } from '@/api/usersApi'
const { hasAuth } = useAuth()
const selectValue = ref<string[]>([])
const keyword = ref<string>('')
const loading = ref(false)
const loadingkh = ref(false)
const detailDialogVisible = ref(false)
const plzyDialogVisible = ref(false)
const AddDialogVisible = ref(false)
const permissionDialog = ref(false)
const roleDialogVisible = ref(false)
// 权限树数据(来自接口 getPermissionMenuList)
const permissionMenuList = ref<any[]>([])
const treeRef = ref()
const isExpandAll = ref(true)
const isSelectAll = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const paginationkh = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const plzyForm = reactive({
customer_id: '',
user_id: ''
})
const zyForm = reactive({
customer_id: ''
})
const addForm = reactive({
id: '',
name: '',
email: '',
phone: '',
report_to: '',
status: ''
})
const roleForm = reactive({
user_id: '',
role_ids: '',
system_ids: ''
})
const userOptions = ref<any>()
//系统
const systemsList = ref<any>()
const getsystemsList = async () => {
const response = await UserService.getsystemsList()
systemsList.value = response || []
}
//角色下拉框
const roleOptions = ref<any>()
const getroleOptions = async () => {
const response = await systemService.getRoleSelectList()
roleOptions.value = response || []
}
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
// 表格数据
const tableData = ref<any[]>([])
const tableDatakh = ref<any[]>([])
onMounted(() => {
getuserList()
gettableList()
})
// tabs 切换联动
watch([selectValue, keyword], () => {
pagination.currentPage = 1
gettableList()
})
const gettableList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize
}
if (selectValue.value) {
params.status = selectValue.value
}
if (keyword.value) {
params.keyword = keyword.value
}
const { records, current, size, total } = await manageService.getUserList(params)
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
const gettableListkh = async (user_id: any) => {
loadingkh.value = true
try {
const { currentPage, pageSize } = paginationkh
const params: any = {
current: currentPage,
size: pageSize,
user_id: user_id
}
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
tableDatakh.value = data.map((item: any) => ({
...item
}))
Object.assign(paginationkh, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loadingkh.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
gettableList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
gettableList()
}
// 处理表格分页变化
const handleSizeChangekh = (newPageSize: number) => {
paginationkh.pageSize = newPageSize
gettableListkh(zyForm.customer_id)
}
const handleCurrentChangekh = (newCurrentPage: number) => {
paginationkh.currentPage = newCurrentPage
gettableListkh(zyForm.customer_id)
}
function statusClass(status: any) {
if (status === 1) return 'status-following'
if (status === 0) return 'status-lost'
return ''
}
const gotransfer = (row: any) => {
paginationkh.currentPage = 1
paginationkh.pageSize = 10
paginationkh.total = 0
gettableListkh(row.id)
zyForm.customer_id = row.id
detailDialogVisible.value = true
}
// 显示角色&系统对话框
const showroleDialog = (row: any) => {
getsystemsList()
getroleOptions()
roleDialogVisible.value = true
roleForm.user_id = row.id
roleForm.role_ids = row.role_ids || []
roleForm.system_ids = row.system_ids || []
}
// 提交角色&系统
const handleRoleSubmit = async () => {
const res = await manageService.userSetRole(roleForm)
ElMessage.success(res?.message || '角色&系统设置成功!')
roleDialogVisible.value = false
gettableList()
}
// 显示权限对话框
const userId = ref('')
const showPermissionDialog = async (row: any) => {
permissionDialog.value = true
userId.value = row.id
try {
const res = await systemService.getPermissionMenuList({ user_id: row.id })
// 兼容不同返回结构(data/records/直接数组)
const list = (res as any)?.data ?? (res as any)?.records ?? (res as any)
permissionMenuList.value = Array.isArray(list) ? list : []
// 根据接口返回的 selected 字段设置默认选中
nextTick(() => {
const keys = buildDefaultCheckedKeys(permissionMenuList.value)
treeRef.value?.setCheckedKeys(keys)
handleTreeCheck()
})
} catch (e) {
permissionMenuList.value = []
}
}
// 构建默认选中的 key 列表(与 node-key="id" 一致)
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
const keys: Array<string | number> = []
const walk = (node: any) => {
if (node?.meta?.selected) {
keys.push(node.id)
}
if (Array.isArray(node?.meta?.authList)) {
node.meta.authList.forEach((auth: any) => {
if (auth?.selected) {
keys.push(`${node.id}__auth__${auth.authMark}`)
}
})
}
if (Array.isArray(node?.children)) node.children.forEach(walk)
}
source.forEach(walk)
return keys
}
// 处理菜单数据,将 authList 转换为子节点
const processedMenuList = computed(() => {
const processNode = (node: any) => {
// 规范化 id,确保树节点唯一键与 node-key 对应
const processed: any = { ...node, id: node.id }
// 如果有 authList,将其转换为子节点
if (node.meta && node.meta.authList && node.meta.authList.length) {
const authNodes = node.meta.authList.map((auth: any) => ({
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
id: `${node.id}__auth__${auth.authMark}`,
name: `${node.name}_${auth.authMark}`,
label: auth.title,
auth_mark: auth.authMark,
auth_id: auth.id,
isAuth: true,
checked: auth.selected || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
// 递归处理子节点
if (processed.children) {
processed.children = processed.children.map(processNode)
}
return processed
}
const source = permissionMenuList.value || []
return source.map(processNode)
})
const defaultProps = {
children: 'children',
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
}
const toggleExpandAll = () => {
const tree = treeRef.value
if (!tree) return
// 使用store.nodesMap直接控制所有节点的展开状态
const nodes = tree.store.nodesMap
for (const node in nodes) {
nodes[node].expanded = !isExpandAll.value
}
isExpandAll.value = !isExpandAll.value
}
const toggleSelectAll = () => {
const tree = treeRef.value
if (!tree) return
if (!isSelectAll.value) {
// 全选:获取所有节点的key并设置为选中
const allKeys = getAllNodeKeys(processedMenuList.value)
tree.setCheckedKeys(allKeys)
} else {
// 取消全选:清空所有选中
tree.setCheckedKeys([])
}
isSelectAll.value = !isSelectAll.value
}
const getAllNodeKeys = (nodes: any[]): Array<string | number> => {
const keys: Array<string | number> = []
const traverse = (nodeList: any[]) => {
nodeList.forEach((node) => {
if (node.id !== undefined && node.id !== null) {
keys.push(node.id)
}
if (node.children && node.children.length > 0) {
traverse(node.children)
}
})
}
traverse(nodes)
return keys
}
const handleTreeCheck = () => {
const tree = treeRef.value
if (!tree) return
// 使用树组件的getCheckedKeys方法获取选中的节点
const checkedKeys = tree.getCheckedKeys()
const allKeys = getAllNodeKeys(processedMenuList.value)
// 判断是否全选:选中的节点数量等于总节点数量
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
}
const getSelectedIds = () => {
const tree = treeRef.value
if (!tree) return { menuIds: [], authIds: [] }
const checkedNodes = tree.getCheckedNodes?.(false) || []
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
const allNodes = [...checkedNodes, ...halfCheckedNodes]
const menuIds = Array.from(
new Set(
allNodes
.filter((n: any) => !n.isAuth && n.id !== undefined && n.id !== null)
.map((n: any) => n.id)
)
)
const authIds = Array.from(
new Set(
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
)
)
const ids = Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
return { menuIds, authIds, ids }
}
const savePermission = async () => {
const { ids } = getSelectedIds()
const res = await manageService.userMenuAuth({
user_id: userId.value,
resources: ids
})
ElMessage.success(res?.message || '权限保存成功')
permissionDialog.value = false
}
// 选中的行数据
const selectedRows = ref<any[]>([])
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
const plzyDialogVisiblefn = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
plzyDialogVisible.value = true
}
// 批量转移
const handleplzySubmit = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
const res = await manageService.crmCustomerTransfer(plzyForm)
ElMessage.success(res?.message || '批量转移成功!')
plzyDialogVisible.value = false
detailDialogVisible.value = false
gettableList()
}
// 编辑用户
const onEdit = (row: any) => {
AddDialogVisible.value = true
addForm.id = row.id
addForm.name = row.name
addForm.email = row.email
addForm.phone = row.phone
addForm.status = row.status
addForm.report_to = row.report_to || ''
}
const handleAddSubmit = async () => {
const res = await manageService.userEdit(addForm)
ElMessage.success(res?.message || '编辑成功!')
AddDialogVisible.value = false
gettableList()
}
// 图片预览相关
const showImageViewer = ref(false)
const currentImageUrl = ref('')
const handleAvatarClick = (avatarUrl: string) => {
if (avatarUrl) {
currentImageUrl.value = avatarUrl
showImageViewer.value = true
}
}
</script>
<style scoped lang="scss">
:deep(.el-badge) {
--el-badge-size: 16px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
}
.itemtag {
margin-top: -16px;
margin-left: -13px;
}
:deep(.el-table__header th) {
background: #f7f9fa !important;
color: #333;
font-weight: bold;
}
:deep(.el-table__empty-block) {
height: 450px !important;
}
:deep(.el-table__body tr.custom-table-row td) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__body tr td:last-child) {
border-right: none !important;
}
:deep(.el-table__body tr:last-child td) {
border-bottom: none !important;
}
:deep(.el-table__header-wrapper th) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__header-wrapper th:last-child) {
border-right: none !important;
}
:deep(.el-table) {
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.customer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.customer-search-group {
display: flex;
gap: 12px;
}
.customer-tags {
background: var(--art-main-bg-color);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.filter-label {
width: 120px;
color: #666;
margin-right: 8px;
flex-shrink: 0;
font-size: 14px;
}
.filter-tags {
flex: 1;
color: #333;
font-weight: 500;
font-size: 14px;
}
.customer-tabs {
margin-bottom: 8px;
}
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.customer-table {
background: var(--art-main-bg-color);
border-radius: 8px;
margin-top: 0;
}
.filter-more-btn {
text-align: center;
margin-top: 8px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.form-submit-row {
text-align: center;
}
</style>
+533
View File
@@ -0,0 +1,533 @@
<template>
<div class="page-content user">
<div class="content">
<div class="left-wrap">
<div class="user-wrap box-style">
<!-- <img class="bg" src="@imgs/user/bg.webp" /> -->
<img
class="avatar"
:src="userInfo.avatar != '' ? userInfo.avatar : '/src/assets/img/user/avatar.webp'"
/>
<h2 class="name">{{ userInfo.name }}</h2>
<p class="des">SN:{{ userInfo.sn }}</p>
<div class="outer-info">
<!-- <div class="info-row">
<span class="info-label">昵 称:</span>
<span class="info-value">{{ userInfo.phone }}</span>
</div> -->
<div class="info-row">
<span class="info-label">电 话:</span>
<span class="info-value">{{ userInfo.phone }}</span>
</div>
<div class="info-row">
<span class="info-label">邮 箱:</span>
<span class="info-value">{{ userInfo.email }}</span>
</div>
<div class="info-row">
<span class="info-label">公 司:</span>
<span class="info-value">{{ userInfo.corp_name }}</span>
</div>
<!-- <div class="info-row">
<span class="info-label">国 家:</span>
<span class="info-value">{{ userInfo.country_txt }}</span>
</div> -->
</div>
</div>
</div>
<div class="right-wrap">
<div class="info box-style">
<h1 class="title"><span class="border" style="margin-right: 5px"></span>基本信息</h1>
<ElForm
:model="form"
class="form"
ref="ruleFormRef"
label-width="86px"
label-position="left"
>
<ElFormItem label="头像" prop="avatar">
<el-upload
class="qrcode-upload"
:action="actionImageUplode"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleUploadavatar"
:disabled="!isEdit"
>
<div class="qrcode-upload-box">
<img
class="avatar"
:src="
imageUrlavatar != '' ? imageUrlavatar : '/src/assets/img/user/avatar.webp'
"
/>
</div>
</el-upload>
</ElFormItem>
<ElRow>
<ElFormItem label="昵称" prop="name">
<el-input v-model="form.name" :disabled="!isEdit" />
</ElFormItem>
<ElFormItem label="性别" prop="sex" class="right-input">
<ElSelect v-model="form.sex" placeholder="未知" :disabled="!isEdit">
<ElOption
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
</ElRow>
<ElRow>
<ElFormItem label="联系方式" prop="phone">
<ElInput v-model="form.phone" disabled />
</ElFormItem>
<ElFormItem label="公司" prop="company" class="right-input">
<ElInput v-model="form.corp_name" disabled />
</ElFormItem>
<!-- <ElFormItem label="职务" prop="position" class="right-input">
<ElInput v-model="form.position" :disabled="!isEdit" />
</ElFormItem> -->
</ElRow>
<!-- <ElRow>
<ElFormItem label="公司" prop="company">
<ElInput v-model="form.corp_name" disabled />
</ElFormItem>
<ElFormItem label="国家" prop="address" class="right-input">
<el-select
v-model="form.country_id"
filterable
placeholder="请选择国家"
:disabled="!isEdit"
>
<el-option
v-for="item in countryOptions"
:key="item.id"
:label="item.cn_addr"
:value="item.id"
/>
</el-select>
</ElFormItem>
</ElRow> -->
<ElRow>
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="form.email" :disabled="!isEdit" />
</ElFormItem>
</ElRow>
<!-- <ElRow>
<ElFormItem
label="二维码"
prop="socialize"
:class="isEdit ? 'qrcode-radio-group' : ''"
>
<el-radio-group v-model="form.socialize" v-if="isEdit">
<el-radio label="微信">微信</el-radio>
<el-radio label="WhatsApp">WhatsApp</el-radio>
</el-radio-group>
<template v-else>
<img
v-if="form.socialize_pic_full"
:src="form.socialize_pic_full"
class="qrcode-img"
/>
<span v-else class="qrcode-upload-text">暂无二维码</span>
</template>
</ElFormItem>
<ElFormItem v-if="isEdit" label="上传二维码" prop="address" class="right-input">
<el-upload
class="qrcode-upload"
:action="actionImageUplode"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleUploadavatarqrcode"
>
<div class="qrcode-upload-box">
<img
v-if="form.socialize_pic_full"
:src="socialize_pic_full"
class="qrcode-img"
/>
<span v-else class="qrcode-upload-text">
<img :src="imageUrls" class="qrcode-img" />
</span>
</div>
</el-upload>
</ElFormItem>
</ElRow> -->
<div class="el-form-item-right">
<ElButton type="primary" style="width: 90px" v-ripple @click="edit">
<i v-if="!isEdit" class="iconfont-sys"> &#xe6bf;</i>
{{ isEdit ? '保存' : '编辑' }}
</ElButton>
</div>
</ElForm>
</div>
<div class="info box-style" style="margin-top: 20px">
<h1 class="title">更改密码</h1>
<ElForm :model="pwdForm" class="form" label-width="86px" label-position="left">
<ElFormItem label="当前密码" prop="password">
<ElInput
v-model="pwdForm.password"
type="password"
:disabled="!isEditPwd"
show-password
/>
</ElFormItem>
<ElFormItem label="新密码" prop="password_new">
<ElInput
v-model="pwdForm.password_new"
type="password"
:disabled="!isEditPwd"
show-password
/>
</ElFormItem>
<ElFormItem label="确认新密码" prop="confirmPassword">
<ElInput
v-model="pwdForm.confirmPassword"
type="password"
:disabled="!isEditPwd"
show-password
/>
</ElFormItem>
<div class="el-form-item-right">
<ElButton type="primary" style="width: 90px" v-ripple @click="editPwd">
<i v-if="!isEditPwd" class="iconfont-sys"> &#xe6bf;</i>
{{ isEditPwd ? '保存' : '编辑' }}
</ElButton>
</div>
</ElForm>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ElForm, FormInstance } from 'element-plus'
// import imageUrl from '@/assets/img/crm/upimg.png'
import { CrmService } from '@/api/crmApi'
import { UserService } from '@/api/usersApi'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { useUserStore } from '@/store/modules/user'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
defineOptions({ name: 'UserCenter' })
// 定义客户详情类型
interface CustomerInfo {
[key: string]: any
}
// const imageUrls = ref(imageUrl)
const imageUrlavatar = ref('')
// const socialize_pic_full = ref('')
// const countryOptions = ref<any>()
const form = ref<CustomerInfo>({})
//获取公司 getCompanyList
// const companyList = ref<any>()
//国家
// const getarea = async () => {
// try {
// // 这里调用获取品牌列表的接口
// const res = await CrmService.getCountry()
// countryOptions.value = res||[]
// } catch (error) { countryOptions.value = [] }
// }
const { getUserInfo } = useUserStore()
const userInfo = ref<CustomerInfo>({})
const getuserInfo = async () => {
// const data = await CrmService.userget()
userInfo.value = getUserInfo.user
form.value = getUserInfo.user
imageUrlavatar.value = getUserInfo.user.avatar
// socialize_pic_full.value = data.socialize_pic_full
}
const isEdit = ref(false)
const isEditPwd = ref(false)
const pwdForm = reactive({
password: '',
password_new: '',
confirmPassword: ''
})
const ruleFormRef = ref<FormInstance>()
const options = [
{
value: '',
label: '未知'
},
{
value: '1',
label: '男'
},
{
value: '2',
label: '女'
}
]
onMounted(async () => {
// await getarea()
await getuserInfo()
})
const edit = async () => {
if (isEdit.value) {
const res = await UserService.editUserInfo(form.value)
ElMessage.success(res?.message || '操作成功')
getuserInfo()
}
isEdit.value = !isEdit.value
}
const editPwd = async () => {
if (isEditPwd.value) {
// 判断新密码和确认密码是否一致
if (pwdForm.password_new !== pwdForm.confirmPassword) {
ElMessage.error('新密码和确认密码不一致')
return
}
const res = await CrmService.savePassword(pwdForm)
ElMessage.success(res?.message || '操作成功')
}
isEditPwd.value = !isEditPwd.value
}
//头像上传
function handleUploadavatar(response: any) {
// 假设返回图片地址字段为 response.url 或 response.data.url
imageUrlavatar.value = response.url || (response.data && response.data.file_url) || ''
// 如需同步到 form.picture
form.value.avatar = response.url || (response.data && response.data.file_name) || ''
}
//二维码上传
// function handleUploadavatarqrcode(response: any) {
// // 假设返回图片地址字段为 response.url 或 response.data.url
// socialize_pic_full.value = response.url || (response.data && response.data.file_url) || ''
// imageUrls.value = response.url || (response.data && response.data.file_url) || ''
// // 如需同步到 form.picture
// form.value.socialize_pic = response.url || (response.data && response.data.file_name) || ''
// }
</script>
<style lang="scss">
.user {
.icon {
width: 1.4em;
height: 1.4em;
overflow: hidden;
vertical-align: -0.15em;
fill: currentcolor;
}
}
</style>
<style lang="scss" scoped>
:deep(.qrcode-radio-group .el-form-item__content){
height: 40px;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.qrcode-img{
width: 80px;
height: 80px;
}
.page-content {
width: 100%;
height: 100%;
padding: 0 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
$box-radius: calc(var(--custom-radius) + 4px);
.box-style {
border: 1px solid var(--art-border-color);
}
.content {
position: relative;
display: flex;
justify-content: space-between;
margin-top: 10px;
.left-wrap {
width: 350px;
margin-right: 25px;
.user-wrap {
position: relative;
height: 400px;
padding: 35px 30px;
overflow: hidden;
text-align: center;
// background: var(--art-main-bg-color);
background: linear-gradient(180deg, #E9F0FF 0%, #FFFFFF 67.9%);
border-radius: $box-radius;
.avatar {
position: relative;
z-index: 10;
width: 80px;
height: 80px;
// margin-top: 120px;
object-fit: cover;
border: 2px solid #fff;
border-radius: 50%;
}
.name {
margin-top: 20px;
font-size: 22px;
font-weight: 400;
}
.des {
margin-top: 20px;
font-size: 14px;
}
.outer-info {
// width: 260px;
margin: 0 auto;
margin-top: 30px;
}
.info-row {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
font-size: 16px;
}
.info-label {
display: inline-block;
width: 100px;
color: #222;
text-align: right;
letter-spacing: 2px;
}
.info-value {
color: #333;
margin-left: 8px;
word-break: break-all;
}
.lables {
margin-top: 40px;
h3 {
font-size: 15px;
font-weight: 500;
}
> div {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 15px;
> div {
padding: 3px 6px;
margin: 0 10px 10px 0;
font-size: 12px;
background: var(--art-main-bg-color);
border: 1px solid var(--art-border-color);
border-radius: 2px;
}
}
}
}
.gallery {
margin-top: 25px;
border-radius: 10px;
.item {
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
.right-wrap {
flex: 1;
overflow: hidden;
border-radius: $box-radius;
.info {
background: var(--art-main-bg-color);
border-radius: $box-radius;
.title {
padding: 15px 25px;
font-size: 20px;
font-weight: 400;
color: var(--art-text-gray-800);
// border-bottom: 1px solid var(--art-border-color);
}
.form {
box-sizing: border-box;
padding: 30px 25px;
> .el-row {
.el-form-item {
width: calc(50% - 10px);
}
.el-input,
.el-select {
width: 100%;
}
}
.right-input {
margin-left: 20px;
}
.el-form-item-right {
display: flex;
align-items: center;
justify-content: end;
.el-button {
width: 110px !important;
}
}
}
}
}
}
}
@media only screen and (max-width: $device-ipad-vertical) {
.page-content {
.content {
display: block;
margin-top: 5px;
.left-wrap {
width: 100%;
}
.right-wrap {
width: 100%;
margin-top: 15px;
}
}
}
}
</style>