v1.0.2 更新内容

This commit is contained in:
浅念
2025-08-15 16:31:55 +08:00
parent f5deb7b8f7
commit 0e84bedb67
21 changed files with 2977 additions and 319 deletions
+23
View File
@@ -175,4 +175,27 @@ export class manageService {
url: '/common/preset/cache/clear' url: '/common/preset/cache/clear'
}) })
} }
//公海客户分配
static crmCustomerAllocation(params: any) {
return request.get<Api.Common.AddResponse>({
url: '/admin/crm/customer/cede',
params
// showErrorMessage: false // 不显示错误消息
})
}
//公海客户跟进记录
static crmCustomerFollow(params: any) {
return request.get<any>({
url: '/admin/crm/customer/follow',
params: params
})
}
//公海客户删除
static crmCustomerDelete(params: any) {
return request.get<Api.Common.AddResponse>({
url: '/admin/crm/customer/delete',
params
// showErrorMessage: false // 不显示错误消息
})
}
} }
+15
View File
@@ -176,4 +176,19 @@ export class CrmService {
// showErrorMessage: false // 不显示错误消息 // showErrorMessage: false // 不显示错误消息
}) })
} }
//所属员工
static getCustomerUserList(params: Api.Common.PaginatingParams) {
return request.get<Api.Common.CommonListData>({
url: '/common/user/team',
params
})
}
//标记离职
static markLeave(params: any) {
return request.get<Api.Work.AddResponse>({
url: '/common/user/resign',
params
// showErrorMessage: false // 不显示错误消息
})
}
} }
+3 -2
View File
@@ -168,9 +168,10 @@ export class systemService {
}) })
} }
//菜单列表 //菜单列表
static getMenutreeList() { static getMenutreeList(params: any) {
return request.get<any>({ return request.get<any>({
url: '/admin/resources/list/tree' url: '/admin/resources/list/tree',
params
}) })
} }
static getMenuList() { static getMenuList() {
+8 -1
View File
@@ -7,7 +7,7 @@ export class UserService {
url: '/common/preset/corps' url: '/common/preset/corps'
}) })
} }
//获取系统 //获取系统
static getsystemsList() { static getsystemsList() {
return request.get<Api.Brand.BrandInfo>({ return request.get<Api.Brand.BrandInfo>({
url: '/common/preset/systems' url: '/common/preset/systems'
@@ -78,4 +78,11 @@ export class UserService {
url: '/common/preset/menus' url: '/common/preset/menus'
}) })
} }
//个人信息编辑
static editUserInfo(params: any) {
return request.post<Api.Work.AddResponse>({
url: '/admin/user/edit',
params
})
}
} }
@@ -163,7 +163,7 @@
</div> </div>
</div> --> </div> -->
<ul class="user-menu" style="border: none"> <ul class="user-menu" style="border: none">
<li @click="goPage('/system/user-center')"> <li @click="gouserInfo()">
<i class="menu-icon iconfont-sys">&#xe734;</i> <i class="menu-icon iconfont-sys">&#xe734;</i>
<span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span> <span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span>
</li> </li>
@@ -223,7 +223,6 @@
import { languageOptions } from '@/locales' import { languageOptions } from '@/locales'
const isWindows = navigator.userAgent.includes('Windows') const isWindows = navigator.userAgent.includes('Windows')
const { locale } = useI18n() const { locale } = useI18n()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const userStore = useUserStore() const userStore = useUserStore()
const router = useRouter() const router = useRouter()
@@ -315,14 +314,22 @@
const goPage = (path: string) => { const goPage = (path: string) => {
router.push(path) router.push(path)
} }
const { systemsid } = useUserStore()
const toDocs = () => { const gouserInfo = () => {
window.open(WEB_LINKS.DOCS) if (systemsid == '1') {
router.push('/admin/user/user_info')
} else {
router.push('/system/user-center')
}
} }
const toGithub = () => { // const toDocs = () => {
window.open(WEB_LINKS.GITHUB) // window.open(WEB_LINKS.DOCS)
} // }
// const toGithub = () => {
// window.open(WEB_LINKS.GITHUB)
// }
const toHome = () => { const toHome = () => {
router.push(home_page) router.push(home_page)
+16 -1
View File
@@ -1,5 +1,12 @@
<template> <template>
<div class="customer-page"> <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"> <div class="boxshadow">
<!-- 表格 --> <!-- 表格 -->
<ArtTable <ArtTable
@@ -15,6 +22,7 @@
lass="customer-table" lass="customer-table"
> >
<template #default> <template #default>
<el-table-column prop="code" label="编号" align="center" />
<el-table-column prop="contacts" label="客户名称" align="center" /> <el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" 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="channel_name" label="渠道" align="center" />
@@ -83,7 +91,7 @@
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { manageService } from '@/api/adminApi' import { manageService } from '@/api/adminApi'
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const selectValue = ref<string[]>([]) const keyword = ref<string>('')
const loading = ref(false) const loading = ref(false)
const detailDialogVisible = ref(false) const detailDialogVisible = ref(false)
const audetDialogVisible = ref(false) const audetDialogVisible = ref(false)
@@ -110,6 +118,10 @@
getuserList() getuserList()
gettableList() gettableList()
}) })
watch([keyword], () => {
pagination.currentPage = 1
gettableList()
})
const gettableList = async () => { const gettableList = async () => {
loading.value = true loading.value = true
try { try {
@@ -119,6 +131,9 @@
size: pageSize, size: pageSize,
scene: 'audit' scene: 'audit'
} }
if (keyword.value) {
params.keyword = keyword.value
}
const { data, current, size, total } = await manageService.getcrmCustomerList(params) const { data, current, size, total } = await manageService.getcrmCustomerList(params)
tableData.value = data.map((item: any) => ({ tableData.value = data.map((item: any) => ({
...item ...item
+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>
+221 -12
View File
@@ -3,17 +3,22 @@
<!-- 顶部操作栏 --> <!-- 顶部操作栏 -->
<div class="customer-toolbar boxshadow"> <div class="customer-toolbar boxshadow">
<div class="customer-search-group"> <div class="customer-search-group">
<el-select <el-form-item label="关键字:" label-width="70px">
v-model="selectValue" <el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
style="min-width: 120px" </el-form-item>
filterable <el-form-item label="跟单人" label-width="70px">
multiple <el-select
collapse-tags v-model="selectValue"
placeholder="请选择跟单人" style="min-width: 120px"
> filterable
<el-option v-for="v in userOptions" :key="v.id" :label="v.name" :value="v.id"> multiple
</el-option> collapse-tags
</el-select> 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> </div>
<el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')"> <el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
<i class="iconfont-sys">&#xe89f;</i> 导出 <i class="iconfont-sys">&#xe89f;</i> 导出
@@ -103,6 +108,7 @@
> >
<template #default> <template #default>
<el-table-column type="selection" width="55px" align="center"></el-table-column> <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="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" 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="channel_name" label="渠道" align="center" />
@@ -142,6 +148,15 @@
> >
<i class="iconfont-sys"> &#xe794;</i>转移 <i class="iconfont-sys"> &#xe794;</i>转移
</el-link> </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> </template>
</el-table-column> </el-table-column>
</template> </template>
@@ -232,6 +247,64 @@
</div> </div>
</el-form> </el-form>
</el-dialog> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -239,6 +312,9 @@
import { CrmService } from '@/api/crmApi' import { CrmService } from '@/api/crmApi'
import { manageService } from '@/api/adminApi' import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import emptyimg from '@/assets/img/crm/empty.png'
const TIMELINE_PLACEMENT = 'top'
const activecolor = '#3471ff'
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const selectValue = ref<string[]>([]) const selectValue = ref<string[]>([])
const showMore = ref(false) const showMore = ref(false)
@@ -246,7 +322,15 @@
const detailDialogVisible = ref(false) const detailDialogVisible = ref(false)
const zyDialogVisible = ref(false) const zyDialogVisible = ref(false)
const plzyDialogVisible = ref(false) const plzyDialogVisible = ref(false)
const followRecordDialogVisible = ref(false)
const detailRow = ref<any>(null) 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({ const pagination = reactive({
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
@@ -294,7 +378,7 @@
tagGrouplistfn() tagGrouplistfn()
}) })
// tabs 切换联动 // tabs 切换联动
watch([selectValue], () => { watch([selectValue, keyword], () => {
pagination.currentPage = 1 pagination.currentPage = 1
gettableList() gettableList()
}) })
@@ -309,6 +393,9 @@
if (selectValue.value && selectValue.value.length) { if (selectValue.value && selectValue.value.length) {
params.user_id = selectValue.value.join(',') params.user_id = selectValue.value.join(',')
} }
if (keyword.value) {
params.keyword = keyword.value
}
// 标签参数,所有选中的标签id组成数组 // 标签参数,所有选中的标签id组成数组
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean) const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
if (selectedTagIds.length) { if (selectedTagIds.length) {
@@ -400,6 +487,27 @@
zyForm.customer_id = row.id zyForm.customer_id = row.id
zyDialogVisible.value = true 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 handlezySubmit = async () => {
const res = await manageService.crmCustomerTransfer({ const res = await manageService.crmCustomerTransfer({
@@ -431,6 +539,16 @@
</script> </script>
<style scoped lang="scss"> <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) { :deep(.el-badge) {
--el-badge-size: 16px; --el-badge-size: 16px;
} }
@@ -616,4 +734,95 @@
max-width: 100% !important; 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> </style>
+47 -18
View File
@@ -4,7 +4,7 @@
<ElRow> <ElRow>
<div style="width: 12px"></div> <div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2"> <ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple>新增</ElButton> <ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@company_add')">新增</ElButton>
</ElCol> </ElCol>
</ElRow> </ElRow>
<ArtTable <ArtTable
@@ -19,6 +19,7 @@
<template #default> <template #default>
<ElTableColumn label="ID" prop="id" /> <ElTableColumn label="ID" prop="id" />
<ElTableColumn label="公司名称" prop="name" /> <ElTableColumn label="公司名称" prop="name" />
<ElTableColumn label="公司标识" prop="abbr" />
<ElTableColumn label="状态" prop="status"> <ElTableColumn label="状态" prop="status">
<template #default="scope"> <template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'"> <ElTag :type="scope.row.status ? 'primary' : 'info'">
@@ -30,8 +31,21 @@
<ElTableColumn fixed="right" label="操作" width="200px"> <ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope"> <template #default="scope">
<ElRow> <ElRow>
<el-button link @click="showDialog('edit', scope.row)"> 编辑 </el-button> <el-button
<el-button link @click="deleteRole(scope.row)"> 删除 </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> </ElRow>
</template> </template>
</ElTableColumn> </ElTableColumn>
@@ -43,10 +57,19 @@
width="40%" width="40%"
align-center align-center
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
<ElFormItem label="公司名称" prop="name"> <ElFormItem label="公司名称" prop="name">
<ElInput v-model="form.name" /> <ElInput v-model="form.name" />
</ElFormItem> </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> </ElForm>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -65,22 +88,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { Ordertype} from '@/mock/temp/formData'
import { systemService } from '@/api/systemApi' import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' }) defineOptions({ name: 'Type ' })
const loading = ref(false) const loading = ref(false)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const typeList = ref<any[]>([])
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const { width } = useWindowSize() const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500) const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({ const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }] name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
abbr: [{ required: true, message: '请输入公司简称', trigger: 'blur' }]
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
}) })
const form = reactive<Ordertype>({ const form = reactive<any>({
name: '', name: '',
id:'' id: '',
abbr: '',
status: 1
}) })
const pagination = reactive({ const pagination = reactive({
currentPage: 1, currentPage: 1,
@@ -127,18 +154,25 @@
if (type === 'edit' && row) { if (type === 'edit' && row) {
form.name = row.name form.name = row.name
form.id = row.id form.id = row.id
form.status = row.status
form.abbr = row.abbr
// form.tenant_key = row.tenant_key
} else { } else {
form.name = '' form.name = ''
form.id = ''
form.status = 1
form.abbr = ''
// form.tenant_key = ''
} }
} }
const deleteRole = (row?: any) => { const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除该分类吗?', '删除确认', { ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'error' type: 'error'
}).then(async () => { }).then(async () => {
const { message } = await systemService.companyDelete({ const { message } = await systemService.companyDelete({
id: row.id, id: row.id
}) })
ElMessage.success(message) ElMessage.success(message)
getWorkList() getWorkList()
@@ -148,17 +182,12 @@
const handleSubmit = async (formEl: FormInstance | undefined) => { const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
if (dialogType.value === 'add') { if (dialogType.value === 'add') {
const { message } = await systemService.companyAdd({ const { message } = await systemService.companyAdd(form)
name: form.name
})
ElMessage.success(message) ElMessage.success(message)
dialogVisible.value = false dialogVisible.value = false
getWorkList() getWorkList()
} else { } else {
const { message } = await systemService.companyEdit({ const { message } = await systemService.companyEdit(form)
name: form.name,
id: form.id
})
ElMessage.success(message) ElMessage.success(message)
dialogVisible.value = false dialogVisible.value = false
getWorkList() getWorkList()
-194
View File
@@ -1,194 +0,0 @@
<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>新增</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="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)"> 编辑 </el-button>
<el-button link @click="deleteRole(scope.row)"> 删除 </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="160px">
<ElFormItem label="分类名称" prop="name">
<ElInput v-model="form.name" />
</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 { Ordertype} from '@/mock/temp/formData'
import { systemService } from '@/api/systemApi'
defineOptions({ name: 'Type ' })
const loading = ref(false)
const dialogVisible = ref(false)
const typeList = ref<any[]>([])
const formRef = ref<FormInstance>()
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({
name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' }
]
})
const form = reactive<Ordertype>({
name: '',
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.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) {
form.name = row.name
form.id = row.id
} else {
form.name = ''
}
}
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
if (dialogType.value === 'add') {
const { message } = await systemService.feishuAppAdd({
name: form.name
})
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
} else {
const { message } = await systemService.feishuAppEdit({
name: form.name,
id: form.id
})
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>
+63 -8
View File
@@ -14,8 +14,25 @@
<!-- 表格头部 --> <!-- 表格头部 -->
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh"> <ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
<template #left> <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> <ElButton
v-auth="'system@menu_add'"
@click="showModel('menu', null)"
v-ripple
style="margin-left: 20px"
>
添加菜单 添加菜单
</ElButton> </ElButton>
<ElButton @click="toggleExpand" v-ripple> <ElButton @click="toggleExpand" v-ripple>
@@ -224,7 +241,7 @@
import { SearchFormItem } from '@/types' import { SearchFormItem } from '@/types'
import { systemService } from '@/api/systemApi' import { systemService } from '@/api/systemApi'
import { UserService } from '@/api/usersApi' import { UserService } from '@/api/usersApi'
const yearVal = ref<any>('')
defineOptions({ name: 'Menus' }) defineOptions({ name: 'Menus' })
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
@@ -233,6 +250,20 @@
const menuList = ref<any[]>([]) const menuList = ref<any[]>([])
const loading = ref(false) 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 systemsList = ref<any>()
const getsystemsList = async () => { const getsystemsList = async () => {
@@ -467,16 +498,37 @@
const tableData = ref<AppRouteRecord[]>([]) const tableData = ref<AppRouteRecord[]>([])
onMounted(() => { onMounted(() => {
getsystemsListtab()
getTableData() getTableData()
}) })
const searchArticleByYear = () => {
getTableData()
}
// const handleSegmentedClick = (item: any) => {
// // 若点击的是当前项也强制刷新
// yearVal.value = item?.value ?? yearVal.value
// getTableData()
// }
const getTableData = async () => { const getTableData = async () => {
loading.value = true loading.value = true
// const { res } = await systemService.getMenutreeList() // 清空旧数据,避免视觉上出现追加
await systemService.getMenutreeList().then((res) => { menuList.value = []
console.log(res) tableData.value = []
menuList.value = res // 并发安全:本次请求标识
}) 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) // console.log(res)
// menuList.value = data // menuList.value = data
@@ -660,6 +712,9 @@
const isExpanded = ref(false) const isExpanded = ref(false)
const tableRef = ref() const tableRef = ref()
// 数据请求并发保护计数器
let requestCounter = 0
const toggleExpand = () => { const toggleExpand = () => {
isExpanded.value = !isExpanded.value isExpanded.value = !isExpanded.value
nextTick(() => { nextTick(() => {
+25 -4
View File
@@ -7,7 +7,7 @@
<div style="width: 12px"></div> <div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2"> <ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple>搜索</ElButton> <ElButton v-ripple>搜索</ElButton>
<ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton> <ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@role_add')">新增角色</ElButton>
</ElCol> </ElCol>
</ElRow> </ElRow>
@@ -36,9 +36,28 @@
<ElTableColumn fixed="right" label="操作" width="200px"> <ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope"> <template #default="scope">
<ElRow> <ElRow>
<el-button link @click="showPermissionDialog(scope.row)"> 菜单权限 </el-button> <el-button
<el-button link @click="showDialog('edit', scope.row)"> 编辑 </el-button> link
<el-button link @click="deleteRole(scope.row)"> 删除 </el-button> @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> </ElRow>
</template> </template>
</ElTableColumn> </ElTableColumn>
@@ -118,6 +137,8 @@
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { formatMenuTitle } from '@/router/utils/utils' import { formatMenuTitle } from '@/router/utils/utils'
import { systemService } from '@/api/systemApi' import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue' // import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
// import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData' // import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
const loading = ref(false) const loading = ref(false)
+330 -53
View File
@@ -8,7 +8,7 @@
<div style="width: 12px"></div> <div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2"> <ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<!-- <ElButton v-ripple>搜索</ElButton> --> <!-- <ElButton v-ripple>搜索</ElButton> -->
<ElButton @click="showDialog('add')" v-ripple>新增</ElButton> <ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@systems_add')">新增</ElButton>
</ElCol> </ElCol>
</ElRow> </ElRow>
@@ -22,36 +22,114 @@
:total="pagination.total" :total="pagination.total"
> >
<template #default> <template #default>
<ElTableColumn label="ID" prop="id" /> <ElTableColumn label="ID" prop="id" width="80" />
<ElTableColumn label="分类名称" prop="name" /> <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"> <ElTableColumn label="状态" prop="status">
<template #default="scope"> <template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'"> <ElTag :type="scope.row.status ? 'primary' : 'info'">
{{ scope.row.status ? '启用' : '禁用' }} {{ scope.row.status ? '启用' : '禁用' }}
</ElTag> </ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" /> <ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" width="200px"> <ElTableColumn fixed="right" label="操作">
<template #default="scope"> <template #default="scope">
<ElRow> <ElRow>
<el-button link @click="showDialog('edit', scope.row)"> 编辑 </el-button> <el-button
<el-button link @click="deleteRole(scope.row)"> 删除 </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> </ElRow>
</template> </template>
</ElTableColumn> </ElTableColumn>
</template> </template>
</ArtTable> </ArtTable>
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'" :title="dialogType === 'add' ? '新增' : '编辑'"
width="40%" width="60%"
align-center align-center
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
<ElFormItem label="分类名称" prop="name"> <ElFormItem label="系统编码" prop="code">
<ElInput v-model="form.name" /> <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> </ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
@@ -70,25 +148,58 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { Ordertype} from '@/mock/temp/formData'
import { systemService } from '@/api/systemApi' import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' }) defineOptions({ name: 'Type ' })
const loading = ref(false) const loading = ref(false)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const typeList = ref<any[]>([]) const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const iconFileList = ref<any[]>([])
const typeList = ref<any[]>([])
const { width } = useWindowSize() const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500) 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>({ const rules = reactive<FormRules>({
name: [ code: [{ required: true, message: '请输入系统编码', trigger: 'blur' }],
{ 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<Ordertype>({ const form = reactive({
code: '',
name: '', name: '',
id:'' type: '',
desc: '',
icon: '',
domain_url: '',
is_role: false,
sort: 0,
status: true,
id: ''
}) })
const pagination = reactive({ const pagination = reactive({
currentPage: 1, currentPage: 1,
@@ -107,40 +218,86 @@
// 获取列表数据 // 获取列表数据
const getWorkList = async () => { const getWorkList = async () => {
loading.value = true loading.value = true
try { try {
const { currentPage, pageSize } = pagination const { currentPage, pageSize } = pagination
const { records, current, size, total } = await systemService.getSystemList({ const { records, current, size, total } = await systemService.getSystemList({
current: currentPage, current: currentPage,
size: pageSize size: pageSize
}) })
tableData.value = records.map((item: any, index: number) => ({ tableData.value = records.map((item: any, index: number) => ({
...item ...item
})) }))
Object.assign(pagination, { currentPage: current, pageSize: size, total }) Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) { } catch (error) {
console.error('获取列表失败:', error) console.error('获取列表失败:', error)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
const dialogType = ref('add') // 系统类型相关辅助函数
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) => { const showDialog = (type: string, row?: any) => {
dialogVisible.value = true dialogVisible.value = true
dialogType.value = type dialogType.value = type as 'add' | 'edit'
if (type === 'edit' && row) { if (type === 'edit' && row) {
form.name = row.name file_url.value = row.icon
form.id = row.id // 编辑模式,填充所有字段
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 { } else {
form.name = '' 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) => { const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除该分类吗?', '删除确认', { ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'error' type: 'error'
@@ -155,22 +312,72 @@
const handleSubmit = async (formEl: FormInstance | undefined) => { const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
if (dialogType.value === 'add') { await formEl.validate(async (valid) => {
const { message } = await systemService.systemAdd({ if (valid) {
name: form.name if (dialogType.value === 'add') {
}) const { message } = await systemService.systemAdd({
ElMessage.success(message) code: form.code,
dialogVisible.value = false name: form.name,
getWorkList() type: form.type,
} else { desc: form.desc,
const { message } = await systemService.systemEdit({ icon: form.icon,
name: form.name, domain_url: form.domain_url,
id:form.id is_role: form.is_role,
}) sort: form.sort,
ElMessage.success(message) status: form.status
dialogVisible.value = false })
getWorkList() 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> </script>
@@ -191,4 +398,74 @@
fill: currentcolor; 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> </style>
+19
View File
@@ -187,6 +187,23 @@
<el-input v-model="addForm.phone" placeholder="请输入电话" /> <el-input v-model="addForm.phone" placeholder="请输入电话" />
</el-form-item> </el-form-item>
</el-col> </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-col :span="24">
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="addForm.status"> <el-select v-model="addForm.status">
@@ -348,6 +365,7 @@
name: '', name: '',
email: '', email: '',
phone: '', phone: '',
report_to: '',
status: '' status: ''
}) })
const roleForm = reactive({ const roleForm = reactive({
@@ -677,6 +695,7 @@
addForm.email = row.email addForm.email = row.email
addForm.phone = row.phone addForm.phone = row.phone
addForm.status = row.status addForm.status = row.status
addForm.report_to = row.report_to || ''
} }
const handleAddSubmit = async () => { const handleAddSubmit = async () => {
const res = await manageService.userEdit(addForm) const res = await manageService.userEdit(addForm)
+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>
+2 -2
View File
@@ -36,11 +36,11 @@
<div class="form"> <div class="form">
<!-- 登录方式切换 tab --> <!-- 登录方式切换 tab -->
<div class="login-type-tabs"> <div class="login-type-tabs">
<span <!-- <span
:class="{ active: activeLoginType === 'password' }" :class="{ active: activeLoginType === 'password' }"
@click="activeLoginType = 'password'" @click="activeLoginType = 'password'"
>账号密码登录</span >账号密码登录</span
> > -->
<span <span
:class="{ active: activeLoginType === 'qrcode' }" :class="{ active: activeLoginType === 'qrcode' }"
@click="activeLoginType = 'qrcode'" @click="activeLoginType = 'qrcode'"
+33 -6
View File
@@ -7,11 +7,25 @@
添加 添加
</el-button> </el-button>
<div class="customer-search-group"> <div class="customer-search-group">
<el-input v-model="searchValue" placeholder="请输入客户名称或电话" /> <el-form-item label="关键字:" label-width="70px">
<el-select v-model="selectValue"> <el-input v-model="searchValue" placeholder="请输入客户名称或电话" />
<el-option v-for="v in selectOptions" :key="v.value" :label="v.label" :value="v.value"> </el-form-item>
</el-option> <el-form-item label="国家" label-width="70px">
</el-select> <el-select v-model="country_id" 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-form-item label="时间" label-width="70px">
<el-select v-model="selectValue" placeholder="请选择时间" style="width: 120px">
<el-option v-for="v in selectOptions" :key="v.value" :label="v.label" :value="v.value">
</el-option>
</el-select>
</el-form-item>
</div> </div>
</div> </div>
@@ -96,6 +110,7 @@
lass="customer-table" lass="customer-table"
> >
<template #default> <template #default>
<el-table-column prop="code" label="编号" align="center" />
<el-table-column prop="contacts" label="客户名称" align="center" /> <el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" 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="channel_name" label="渠道" align="center" />
@@ -175,6 +190,7 @@
const searchValue = ref('') const searchValue = ref('')
const selectValue = ref('') const selectValue = ref('')
const activeTab = ref('') const activeTab = ref('')
const country_id = ref('')
const showMore = ref(false) const showMore = ref(false)
const loading = ref(false) const loading = ref(false)
const detailDialogVisible = ref(false) const detailDialogVisible = ref(false)
@@ -190,6 +206,15 @@
{ label: '近三天', value: '1' }, { label: '近三天', value: '1' },
{ label: '近七天', value: '2' } { label: '近七天', value: '2' }
] ]
const countryOptions = ref<any>()
//国家
const getarea = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getCountry()
countryOptions.value = res||[]
} catch (error) { countryOptions.value = [] }
}
// 获取列表数据 // 获取列表数据
const tagActiveMap = reactive<{ [groupId: string]: string }>({}) const tagActiveMap = reactive<{ [groupId: string]: string }>({})
//获取标签 //获取标签
@@ -212,11 +237,12 @@
// 表格数据 // 表格数据
const tableData = ref<any[]>([]) const tableData = ref<any[]>([])
onMounted(() => { onMounted(() => {
getarea()
gettableList() gettableList()
tagGrouplistfn() tagGrouplistfn()
}) })
// tabs 切换联动 // tabs 切换联动
watch([activeTab, searchValue, selectValue], () => { watch([activeTab, searchValue, selectValue, country_id], () => {
pagination.currentPage = 1 pagination.currentPage = 1
gettableList() gettableList()
}) })
@@ -230,6 +256,7 @@
} }
if (searchValue.value) params.keyword = searchValue.value if (searchValue.value) params.keyword = searchValue.value
if (selectValue.value) params.follow_last = selectValue.value if (selectValue.value) params.follow_last = selectValue.value
if (country_id.value) params.country_id = country_id.value
if (activeTab.value && activeTab.value !== '') params.follow = activeTab.value if (activeTab.value && activeTab.value !== '') params.follow = activeTab.value
// 标签参数,所有选中的标签id组成数组 // 标签参数,所有选中的标签id组成数组
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean) const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
@@ -2,15 +2,24 @@
<div class="customer-page"> <div class="customer-page">
<div class="customer-toolbar boxshadow"> <div class="customer-toolbar boxshadow">
<div class="customer-search-group"> <div class="customer-search-group">
<el-input v-model="keyword" placeholder="请输入关键字" /> <el-form-item label="关键字:" label-width="70px">
<el-select <el-input v-model="keyword" placeholder="请输入关键字" />
v-model="selectValue" </el-form-item>
style="min-width: 120px" <el-form-item label="组员:" label-width="70px">
filterable <el-select
placeholder="请选择用户" v-model="selectValue"
> style="min-width: 120px"
<el-option v-for="v in userOptions" :key="v.id" :label="v.name" :value="v.id"></el-option> filterable
</el-select> 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> </div>
</div> </div>
<div class="table boxshadow"> <div class="table boxshadow">
@@ -32,6 +41,7 @@
> >
<template #default> <template #default>
<el-table-column type="selection" width="55px" align="center"></el-table-column> <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="客户名称"></el-table-column> <el-table-column prop="contacts" label="客户名称"></el-table-column>
<el-table-column prop="phone" label="电话"></el-table-column> <el-table-column prop="phone" label="电话"></el-table-column>
<el-table-column prop="WhatsApp" label="WhatsApp"></el-table-column> <el-table-column prop="WhatsApp" label="WhatsApp"></el-table-column>
+313
View File
@@ -0,0 +1,313 @@
<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 == 1 ? '在职' : '离职' }}
</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="warning"
underline="never"
style="margin-right: 8px"
@click="statusfn(scope.row)"
v-if="hasAuth('crm_customer@status') && scope.row.status == 1"
>
<i class="iconfont-sys">&#xe641;</i>标记离职
</el-link>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
<!-- 图片预览组件 -->
<ElImageViewer
v-if="showImageViewer"
:url-list="[currentImageUrl]"
:hide-on-click-modal="true"
@close="showImageViewer = false"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { ElImageViewer } from 'element-plus'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const selectValue = ref<string[]>([])
const keyword = ref<string>('')
const loading = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
onMounted(() => {
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 CrmService.getCustomerUserList(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 handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
gettableList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
gettableList()
}
function statusClass(status: any) {
if (status === 1) return 'status-following'
if (status === 0) return 'status-lost'
return ''
}
// 图片预览相关
const showImageViewer = ref(false)
const currentImageUrl = ref('')
const handleAvatarClick = (avatarUrl: string) => {
if (avatarUrl) {
currentImageUrl.value = avatarUrl
showImageViewer.value = true
}
}
const statusfn = (row?: any) => {
ElMessageBox.confirm('确定标记离职吗?', '标记确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await CrmService.markLeave({
user_id: row.id
})
ElMessage.success(message)
gettableList()
})
}
</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>
+286
View File
@@ -0,0 +1,286 @@
<template>
<div class="system-notification-demo">
<h2>浏览器原生Notification API测试Demo</h2>
<div class="permission-status">
<p
>当前通知权限状态: <strong>{{ permissionStatus }}</strong></p
>
<el-button v-if="permissionStatus !== 'granted'" @click="requestPermission" type="primary">
请求通知权限
</el-button>
</div>
<div class="button-group">
<el-button @click="showBasicNotification">基本通知</el-button>
<el-button @click="showIconNotification">带图标通知</el-button>
<el-button @click="showPersistentNotification">持久化通知</el-button>
<el-button @click="showDataNotification">带数据的通知</el-button>
<el-button @click="showEventNotification">带事件监听的通知</el-button>
<el-button @click="checkSupport">检查浏览器支持</el-button>
</div>
<div class="log-area">
<h3>操作日志</h3>
<ul>
<li v-for="(log, index) in logs" :key="index">{{ log }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// import { ElButton } from 'element-plus'
// 状态变量
const permissionStatus = ref('default')
const logs = ref([])
const isSupported = ref(false)
// 添加日志
const addLog = (message) => {
const timestamp = new Date().toLocaleTimeString()
logs.value.unshift(`[${timestamp}] ${message}`)
// 限制日志数量
if (logs.value.length > 10) {
logs.value.pop()
}
}
// 检查浏览器支持情况
const checkSupport = () => {
if ('Notification' in window) {
isSupported.value = true
addLog('浏览器支持原生Notification API')
addLog(`当前权限状态: ${Notification.permission}`)
} else {
isSupported.value = false
addLog('⚠️ 浏览器不支持原生Notification API')
}
}
// 请求通知权限
const requestPermission = async () => {
if (!('Notification' in window)) {
addLog('浏览器不支持原生Notification API')
return
}
try {
const permission = await Notification.requestPermission()
permissionStatus.value = permission
if (permission === 'granted') {
addLog('用户已授予通知权限')
} else if (permission === 'denied') {
addLog('用户拒绝了通知权限')
} else {
addLog('用户暂时未授予通知权限')
}
} catch (error) {
addLog(`请求权限时出错: ${error.message}`)
}
}
// 显示基本通知
const showBasicNotification = () => {
if (!('Notification' in window)) {
addLog('浏览器不支持原生Notification API')
return
}
if (Notification.permission !== 'granted') {
addLog('请先授予通知权限')
return
}
try {
const notification = new Notification('基本通知', {
body: '这是一个使用浏览器原生API的系统通知'
})
notification.onclick = () => {
addLog('用户点击了基本通知')
notification.close()
}
addLog('已发送基本通知')
} catch (error) {
addLog(`发送通知失败: ${error.message}`)
}
}
// 显示带图标通知
const showIconNotification = () => {
if (Notification.permission !== 'granted') {
addLog('请先授予通知权限')
return
}
try {
const notification = new Notification('带图标通知', {
body: '这个通知包含自定义图标',
icon: 'https://element-plus.org/images/element-plus-logo.svg', // 替换为实际图标URL
badge: 'https://element-plus.org/images/element-plus-logo.svg' // 小图标(移动端)
})
addLog('已发送带图标通知')
} catch (error) {
addLog(`发送通知失败: ${error.message}`)
}
}
// 显示持久化通知
const showPersistentNotification = () => {
if (Notification.permission !== 'granted') {
addLog('请先授予通知权限')
return
}
try {
const notification = new Notification('持久化通知', {
body: '这个通知需要用户手动关闭',
requireInteraction: true, // 持久化显示,直到用户交互
icon: 'https://element-plus.org/images/element-plus-logo.svg'
})
notification.onclose = () => {
addLog('用户关闭了持久化通知')
}
addLog('已发送持久化通知')
} catch (error) {
addLog(`发送通知失败: ${error.message}`)
}
}
// 显示带数据的通知
const showDataNotification = () => {
if (Notification.permission !== 'granted') {
addLog('请先授予通知权限')
return
}
try {
const notification = new Notification('带数据的通知', {
body: '这个通知包含自定义数据',
data: {
url: 'https://example.com',
id: 12345,
message: '这是附加数据'
},
icon: 'https://element-plus.org/images/element-plus-logo.svg'
})
notification.onclick = () => {
addLog(`通知数据: ${JSON.stringify(notification.data)}`)
window.open(notification.data.url)
notification.close()
}
addLog('已发送带数据的通知')
} catch (error) {
addLog(`发送通知失败: ${error.message}`)
}
}
// 显示带事件监听的通知
const showEventNotification = () => {
if (Notification.permission !== 'granted') {
addLog('请先授予通知权限')
return
}
try {
const notification = new Notification('带事件监听的通知', {
body: '这个通知演示各种事件监听',
icon: 'https://element-plus.org/images/element-plus-logo.svg'
})
notification.onshow = () => {
addLog('通知显示事件触发')
}
notification.onclick = () => {
addLog('通知点击事件触发')
notification.close()
}
notification.onclose = () => {
addLog('通知关闭事件触发')
}
notification.onerror = (error) => {
addLog(`通知错误事件: ${error.message}`)
}
addLog('已发送带事件监听的通知')
} catch (error) {
addLog(`发送通知失败: ${error.message}`)
}
}
// 页面加载时检查权限状态
onMounted(() => {
addLog('页面加载完成,初始化Demo')
// 检查浏览器支持
checkSupport()
// 检查当前权限状态
if ('Notification' in window) {
permissionStatus.value = Notification.permission
addLog(`初始化权限状态: ${Notification.permission}`)
// 监听权限变化
if (Notification.permissionChange) {
Notification.permissionChange.then((permission) => {
permissionStatus.value = permission
addLog(`通知权限已变更为: ${permission}`)
})
}
}
})
</script>
<style scoped>
.system-notification-demo {
max-width: 800px;
margin: 20px auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h2, h3 {
color: #333;
text-align: center;
}
.permission-status {
margin: 20px 0;
padding: 15px;
background-color: #f5f7fa;
border-radius: 8px;
text-align: center;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin: 30px 0;
}
.log-area {
margin-top: 30px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 8px;
}
.log-area ul {
list-style-type: none;
padding: 0;
max-height: 200px;
overflow-y: auto;
}
.log-area li {
padding: 5px 0;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.log-area li:first-child {
color: #409eff;
}
</style>