Squashed commit of the following:

commit a514bf4009
Author: 浅念 <email@sample.com>
Date:   Thu Sep 18 09:55:10 2025 +0800

    Squashed commit of the following:

    commit 9e260ddcd8
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:49:33 2025 +0800

        修改 kpi

    commit 6feb3ace75
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:42:44 2025 +0800

        kpi 统计

    commit ec257a246d
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 17:04:35 2025 +0800

        修改 弹框表格

    commit fa764852ab
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:54:35 2025 +0800

        添加 账户简称

    commit 65f7f4a123
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:10:28 2025 +0800

        品牌加热门  修改 仪表盘接口

    commit 922cfec81d
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 16 15:57:17 2025 +0800

        调整备货单  crm 端页面

    commit 5c8df36f12
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 15 17:14:38 2025 +0800

        调整备货单 及各页面名称

    commit 5503a14408
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 18:10:03 2025 +0800

        账户添加公司

    commit 3439b45d9a
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:53:26 2025 +0800

        修改终版备货单

    commit 045d13eb7b
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:49:07 2025 +0800

        修改备货单 等页面

    commit da690dfa06
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 11 17:44:37 2025 +0800

        修改 备货单 详情

    commit 853e9591b3
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 10 18:11:39 2025 +0800

        修改 备货单页面

    commit 9052bd1027
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 9 20:53:30 2025 +0800

        备货单  审核功能

    commit 079d9babb8
    Merge: ebd7479 91d810b
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 9 14:51:43 2025 +0800

        Merge branch 'feature/crm_pc' of http://219.157.255.213:25311/xinyouhe/art-design into feature/crm_vehicle

    commit ebd7479573
    Merge: 64e53b6 459d306
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 9 13:57:28 2025 +0800

        Merge branch 'feature/crm_pc' of http://219.157.255.213:25311/xinyouhe/art-design into feature/crm_vehicle

    commit 64e53b6ee9
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 8 19:09:55 2025 +0800

        备货单 加 审批流程

    commit 9bd7c89cb4
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 4 15:09:17 2025 +0800

        车辆信息,备货单  等页面

commit 91d810b33a
Author: 浅念 <email@sample.com>
Date:   Tue Sep 9 14:51:23 2025 +0800

    添加公司搜索

commit 459d306504
Author: 浅念 <email@sample.com>
Date:   Tue Sep 9 13:56:43 2025 +0800

    用户添加 公司搜索

commit 8a0d4fc516
Author: 浅念 <email@sample.com>
Date:   Thu Sep 4 14:38:09 2025 +0800

    自定义跟进时间  处理

commit 58f7d56f40
Author: 浅念 <email@sample.com>
Date:   Wed Sep 3 15:13:10 2025 +0800

    工单去掉 移动端不显示限制

commit a91e1d7820
Author: 浅念 <email@sample.com>
Date:   Tue Sep 2 14:13:15 2025 +0800

    将编号 后移

commit a9f8c5c4dc
Author: 浅念 <email@sample.com>
Date:   Wed Aug 27 09:20:56 2025 +0800

    客户重复通知

commit aefd43f79d
Author: 浅念 <email@sample.com>
Date:   Tue Aug 26 09:55:42 2025 +0800

    修改 通知详情样式  和 即时通讯

commit 1bd9d163ec
Author: 浅念 <email@sample.com>
Date:   Mon Aug 25 15:31:14 2025 +0800

    通知 列表富文本内容显示

commit be6093e57d
Author: 浅念 <email@sample.com>
Date:   Mon Aug 25 13:34:13 2025 +0800

    crm 客户添加自定义列  跟进添加图片粘贴上传

commit d83246ba8c
Author: 浅念 <email@sample.com>
Date:   Fri Aug 22 18:33:58 2025 +0800

    显示编号

commit 329acaf68d
Author: 浅念 <email@sample.com>
Date:   Fri Aug 22 18:27:26 2025 +0800

    通知板块 及 功能调整

commit b319944007
Author: 浅念 <email@sample.com>
Date:   Thu Aug 21 11:12:12 2025 +0800

    调整联系方式字段为电话  列表中多个联系方式纵向排列显示   会增加公司信息的自定义输入框  排序搜索 等功能

commit 70d26f0829
Author: 浅念 <email@sample.com>
Date:   Tue Aug 19 16:18:15 2025 +0800

    1.0.3  的相关功能

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

    v1.0.2 更新内容

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

    添加菜单 权限 等页面

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

    添加工单附件  及样式

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

    添加crm  归属客户

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

    询单权限校验

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

    添加 询单图片预览

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

    修改用户页面 询单

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

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

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

    修改 仪表盘页面样式

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

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

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

    添加echarts 插件

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

    仪表盘接口

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

    修改 仪表盘

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

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

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

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

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

    修改公海和询单页面

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

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

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

    设置默认页面

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

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

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

    修改工单 客户等页面

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

    修改默认值

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

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

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

    回收站添加占位

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

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

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

    修改字段名称

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

    调整代码格式

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

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

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

    对接询单接口

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

    修改代码格式

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

    设置客户列表权限

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

    feat(router): 调整菜单

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

    feat(crmApi.ts): 调整结构

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

    feat(router): 路由调整

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

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

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

    询单页面及菜单

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

    Merge branch 'master' into feature/crm_pc

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

    Merge branch 'master' into feature/crm_pc

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

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

    添加系统码

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

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

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

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

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

    feat(crm): 添加相关图片

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

    Finish crm_pc

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

    crm 接口对接

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

    添加上传头部信息

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

    修改页面

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

    修改页面

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

    修改菜单问题

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

    修改样式

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

    修改工单

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

    添加登录

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

    项目创建

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

    删除

# Conflicts:
#	src/App.vue
#	src/api/adminApi.ts
#	src/api/crmApi.ts
#	src/api/menuApi.ts
#	src/api/systemApi.ts
#	src/api/usersApi.ts
#	src/components/core/layouts/art-header-bar/index.vue
#	src/components/core/layouts/art-page-content/index.vue
#	src/router/routes/asyncRoutes.ts
#	src/router/routesAlias.ts
#	src/views/admin/customer/customeraudit.vue
#	src/views/admin/customer/highseas.vue
#	src/views/admin/customer/index.vue
#	src/views/admin/inquiry_system/index.vue
#	src/views/admin/system/company.vue
#	src/views/admin/system/feishu_app.vue
#	src/views/admin/system/menu/index.vue
#	src/views/admin/system/role/index.vue
#	src/views/admin/system/systems.vue
#	src/views/admin/user/index.vue
#	src/views/admin/user/user_info.vue
#	src/views/auth/login/index.vue
#	src/views/crmpc/customer/Add.vue
#	src/views/crmpc/customer/Edit.vue
#	src/views/crmpc/customer/Followadd.vue
#	src/views/crmpc/customer/Show.vue
#	src/views/crmpc/customer/index.vue
#	src/views/crmpc/highseas/index.vue
#	src/views/crmpc/recycle/index.vue
#	src/views/crmpc/team/team_customer.vue
#	src/views/crmpc/team/user_customer.vue
#	src/views/dashboard/console/index.vue
#	src/views/system/user-center/index.vue
#	src/views/work/myOrder/index.vue
#	src/views/work/order/index.vue
This commit is contained in:
浅念
2025-09-18 10:57:17 +08:00
parent 5d906087d3
commit c7cdec27e0
100 changed files with 31463 additions and 1997 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+283
View File
@@ -0,0 +1,283 @@
<template>
<div class="color-page">
<div class="color-toolbar boxshadow">
<div class="color-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getColorList"
/>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="addfn()" v-ripple v-if="hasAuth('approval_list@add')" type="primary">
新增审核流程
</ElButton>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="名称" prop="name" />
<ElTableColumn label="标识" prop="type" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.is_active ? 'success' : 'info'">
{{ scope.row.is_active ? '启用' : '停用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" width="140px">
<template #default="scope">
<ElRow>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button
type="danger"
link
@click="deleteColor(scope.row)"
v-if="hasAuth('approval_list@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog v-model="dialogVisible" title="新增审批流程" align-center width="600px">
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<ElFormItem label="名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入名称" />
</ElFormItem>
<ElFormItem label="标识" prop="type">
<ElInput v-model="form.type" placeholder="请输入标识" />
</ElFormItem>
<ElFormItem label="超时时间" prop="timeout_hours">
<ElInput v-model="form.timeout_hours" placeholder="请输入超时时间" >
<template #suffix>
<span>h</span>
</template>
</ElInput>
</ElFormItem>
<ElFormItem label="超时动作" prop="timeout_action">
<el-select v-model="form.timeout_action" placeholder="请选择颜色类型" style="width: 100%">
<el-option label="外观颜色" :value="1" />
<el-option label="内饰颜色" :value="2" />
</el-select>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.is_active" :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>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { router } from '@/router'
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminApprovalList' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const typeFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
// 表单数据
const form = reactive<any>({
name: '',
type: '',
timeout_hours: '',
timeout_action: '',
is_active: 1
})
// 表单验证规则
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getColorList()
})
// 监听搜索条件变化
watch([keyword, typeFilter, statusFilter], () => {
pagination.currentPage = 1
getColorList()
})
// 新增按钮点击
const addfn = () => {
router.push('/admin/approval/approval_add')
}
// 获取颜色列表
const getColorList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await manageService.getstocklist(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取颜色列表失败:', error)
ElMessage.error('获取颜色列表失败')
} finally {
loading.value = false
}
}
// 重置搜索条件
const resetSearch = () => {
keyword.value = ''
typeFilter.value = undefined
statusFilter.value = undefined
pagination.currentPage = 1
getColorList()
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getColorList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getColorList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
router.push(`/admin/approval/approval_edit?id=${row.id}`)
}
// 删除
const deleteColor = (row: any) => {
ElMessageBox.confirm('确定删除该审核流程吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await manageService.stockapprovaldelete({ id: row.id })
ElMessage.success(message)
getColorList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
// 新增
const { message } = await manageService.stockapproval(form)
ElMessage.success(message)
dialogVisible.value = false
getColorList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.color-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.color-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.color-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+404
View File
@@ -0,0 +1,404 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<!-- <div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div> -->
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</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"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120" />
<ElTableColumn label="审核人" prop="approver_name" min-width="120" />
<ElTableColumn label="状态" prop="status_desc" min-width="120" />
<ElTableColumn label="审批时间" prop="approved_at" min-width="120" />
<ElTableColumn label="创建时间" prop="created_at" min-width="120" />
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
type="primary"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看审批
</el-link>
<el-link
v-if="scope.row.status == 'pending'"
type="success"
underline="never"
@click="handleCreateRepair(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 快速审批
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 快速审批弹出框 -->
<el-dialog
v-model="approvalDialogVisible"
title="合塑实业-汽车事业部-备货单(客户下定)"
width="80%"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="approval-dialog"
>
<div class="approval-content">
<!-- 头部信息 -->
<OrderDetailContent :orderData="orderDialog.orderData" />
<!-- 审批备注 -->
<div class="info-section">
<ElRow :gutter="12">
<ElCol :span="24">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
</ElCol>
</ElRow>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleApprove('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
>同意</ElButton
>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'AdminStockingPage' })
const orderDialog = reactive({
orderData: {} as OrderData
})
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// 审批弹出框相关
const approvalDialogVisible = ref(false)
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const approvalNotes = ref('')
const approvalAction = ref('')
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await manageService.stockapprovalrecord(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const handleCreate = () => {
// TODO: 跳转到创建备货单页面
router.push('/crmpc/stocking_list/Add')
}
const handleView = (row: any) => {
router.push({
path: '/admin/approval/stocking_auth_detil',
query: { id: row.id, biz_id: row.biz_id, instance_id: row.instance_id, status: row.status } // 可带客户名等参数
})
}
const handleView1 = (row: any) => {
router.push('/crmpc/stocking_list/final_show')
// TODO: 跳转或弹窗查看备货单
}
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
const getDictItemSelectlist = ref<any[]>([])
const getDictItemSelect = async () => {
const res = await commonService.getDictItemSelect({ dict_id: '18' })
getDictItemSelectlist.value = res
}
const instance_id = ref('')
const handleCreateRepair = async (row: any) => {
instance_id.value = row.instance_id
const res = await manageService.checkStock({ id: row.biz_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
approvalDialogVisible.value = true
}
// 审批操作
const handleApprove = async (approvalAction: string) => {
try {
// 这里调用审批API
const { message } = await manageService.stockapprovalpass({
instance_id: instance_id.value,
action: approvalAction,
comment: approvalNotes.value
})
ElMessage.success(message)
approvalDialogVisible.value = false
approvalNotes.value = ''
instance_id.value = ''
// 刷新列表
getList()
} catch (error) {
console.error('审批失败:', error)
ElMessage.error('审批失败')
}
}
</script>
<style scoped lang="scss">
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
// 审批弹出框样式
.approval-dialog {
.approval-content {
max-height: 70vh;
overflow-y: auto;
}
.header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
color: #333333;
.mr40 { margin-right: 40px; }
}
.info-section {
margin-bottom: 24px;
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
.title-bar {
width: 4px;
height: 20px;
background: #409eff;
margin-right: 8px;
border-radius: 2px;
}
span {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
}
.order-items {
.order-item {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
.item-header {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
.item-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.item-details {
.detail-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 12px;
.detail-item {
display: flex;
flex-direction: column;
label {
margin-bottom: 6px;
font-size: 14px;
font-weight: 500;
color: #606266;
}
}
}
}
}
}
.summary-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f0f9ff;
border-radius: 8px;
margin-bottom: 24px;
.summary-item {
.summary-label {
font-weight: 600;
color: #606266;
margin-right: 8px;
}
.summary-value {
font-size: 18px;
font-weight: 700;
color: #409eff;
}
}
}
.approval-notes {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: space-around;
width: 500px;
margin: auto;
gap: 12px;
.reject-btn {
border-color: #409eff;
color: #409eff;
background: white;
&:hover {
background: #ecf5ff;
}
}
.approve-btn {
background: #409eff;
border-color: #409eff;
}
}
}
</style>
@@ -0,0 +1,541 @@
<template>
<div class="stocking-show-page">
<div class="boxshadow">
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">审批记录</span>
</div>
<div class="approval-timeline">
<div v-for="(rec, idx) in approvalRecords" :key="idx" class="timeline-item">
<div class="left">
<div class="avatar-box">
<span class="initial">{{ rec.name?.slice(0, 1) || '—' }}</span>
<i v-if="rec.approved" class="status-dot iconfont-sys">&#xe621;</i>
<i v-if="rec.rejected" class="status-dot rejected">&#xe83a;</i>
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys">&#xe839;</i>
</div>
</div>
<div class="right">
<div class="row name-role">
<span class="name">{{ rec.name }}</span>
<span class="role">({{ rec.role || rec.type || '审批人' }})</span>
</div>
<div class="row time">{{ rec.time }}</div>
</div>
</div>
<div v-if="!approvalRecords.length" class="empty">暂无审批记录</div>
</div>
</div>
<div class="boxshadow" v-if="status == 'pending'">
<div class="form-title">
<span class="border"></span>
<span class="title">审批</span>
</div>
<div class="">
<ElRow :gutter="12">
<ElCol :span="24">
<ElFormItem label="备注:">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="请输入备注信息"
class="approval-notes"
/>
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<div class="form-card" v-if="status == 'pending'">
<!-- 底部信息 -->
<div class="submit-wrap">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleSubmit('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
>同意</ElButton
>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
import { manageService } from '@/api/adminApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
import { router } from '@/router'
defineOptions({ name: 'AdminStockingAuthDetil' })
const { pdf, frameRef, download } = usePdfPreview()
const route = useRoute()
// const approval_id = route.query.id
const stocking_id = route.query.biz_id
const instance_id = route.query.instance_id
const status = route.query.status
const approvalNotes = ref('')
const orderDialog = reactive({
orderData: {} as OrderData
})
const handleSubmit = async (approvalAction: string) => {
try {
const { message } = await manageService.stockapprovalpass({
instance_id: instance_id,
action: approvalAction,
comment: approvalNotes.value
})
router.push('/admin/approval/stocking_auth')
ElMessage.success(message)
} catch {
ElMessage.error('提交失败')
}
}
// 审批记录(先填充假数据展示)
const approvalRecords = ref<any[]>([])
const getDictItemSelectlist = ref<any[]>([])
const getDictItemSelect = async () => {
const res = await commonService.getDictItemSelect({ dict_id: '18' })
getDictItemSelectlist.value = res
}
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await manageService.checkStock({ id: stocking_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
// 审批记录(兼容多种字段)
const resstockapprovallogs = await manageService.stockapprovallogs({ instance_id: instance_id })
console.log(resstockapprovallogs)
const historyList = resstockapprovallogs
if (historyList.length > 0) {
approvalRecords.value = historyList.map((h: any) => ({
name: h?.initiator_name ? h?.initiator_name : h?.name,
role: h?.index == -1 ? '申请人' : '审批人',
time: h?.finished_at || '',
approved: h?.status === 'approved' || h?.status === 'submitted',
rejected: h?.status === 'rejected',
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted'
}))
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getDictItemSelect()
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page {
min-height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 {
margin-right: 40px;
}
}
.form-title {
font-weight: bold;
color: #222;
margin-bottom: 15px;
}
.form-title .title {
margin-left: 6px;
}
.items-wrap {
margin-top: 6px;
}
.item-row {
padding: 8px 0;
}
:deep(.el-input.is-disabled .el-input__inner) {
color: var(--el-text-color-regular);
}
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
.submit-wrap {
display: flex;
justify-content: space-around;
padding: 24px 0 8px;
width: 500px;
margin: auto;
}
/* 审批记录 时间线 */
.approval-timeline {
padding: 8px 4px 8px 0;
}
.timeline-item {
display: flex;
align-items: flex-start;
gap: 10px;
position: relative;
}
.timeline-item + .timeline-item {
margin-top: 50px;
}
.left {
width: 46px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.avatar-box {
width: 46px;
height: 46px;
background: #3d7bff;
border-radius: 10px;
color: #fff;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 20px;
}
.status-dot {
position: absolute;
right: -4px;
bottom: -4px;
// width: 14px;
// height: 14px;
font-size: 9px;
padding: 3px 1px;
border-radius: 50%;
background: #22c55e;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
}
.status-dot.pending {
background: #3471ff;
padding: 3px 3px;
}
.status-dot.rejected {
background: #ff0000;
padding: 3px 3px;
}
/* 连接下一个节点的虚线 */
.timeline-item:not(:last-child) .left::after {
content: '';
position: absolute;
top: 52px; /* avatar 46px + 间距 6px */
left: 23px; /* 盒子宽度一半,确保竖线居中 */
width: 0;
height: calc(100% + 16px); /* 加上相邻项之间的间距 */
border-left: 2px dotted #bfbfbf;
}
.right {
flex: 1;
padding-top: 2px;
}
.row.name-role {
color: #222;
}
.name {
font-weight: 600;
}
.role {
color: #666;
margin-left: 4px;
}
.time {
color: #888;
font-size: 12px;
margin-top: 2px;
}
.empty {
color: #999;
padding: 8px 0 4px 4px;
}
</style>
+157 -7
View File
@@ -22,14 +22,24 @@
lass="customer-table"
>
<template #default>
<el-table-column prop="code" label="编号" align="center" />
<el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
<el-table-column prop="channel_name" label="渠道" align="center" />
<el-table-column prop="country_name" label="国家" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<!-- <el-table-column prop="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="email" label="邮箱" align="center" /> -->
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
@@ -40,6 +50,13 @@
</el-table-column>
<el-table-column prop="user_name" label="标记人"></el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column
prop="code"
label="编号"
align="center"
min-width="120px"
show-overflow-tooltip
/>
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
@@ -70,32 +87,120 @@
<el-dialog v-model="detailDialogVisible" title="详情">
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<el-dialog v-model="audetDialogVisible" title="审核" width="360px">
<el-form :model="audetForm" label-width="0">
<el-form-item>
<el-dialog v-model="audetDialogVisible" title="审核" width="700px">
<!-- 客户标记无效 原因与附件回显 -->
<div style="margin-bottom: 16px">
<div style="margin-bottom: 8px">
<span>标记原因:</span>
<span>{{ reportRemark || '-' }}</span>
</div>
<div>
<span>附件说明:</span>
<div
v-if="reportAttachments.length"
style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px"
>
<div
v-for="(att, idx) in reportAttachments"
:key="idx"
style="
display: flex;
align-items: center;
gap: 6px;
border: 1px solid #f0f0f0;
padding: 6px 8px;
border-radius: 6px;
"
>
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(att.url)">
<img
:src="att.url"
:alt="att.name"
style="
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
"
@click="openImagePreview(att.url)"
/>
</template>
<template v-else>
<i class="iconfont-sys">&#xe7b8;</i>
</template>
<el-link
v-if="!/\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(att.url)"
type="primary"
:underline="false"
@click="downloadAttachment(att)"
>下载</el-link
>
</div>
</div>
<el-empty v-else description="暂无附件" style="--el-empty-padding: 10px 0" />
</div>
</div>
<el-form :model="audetForm" label-width="50px">
<el-form-item label="操作">
<el-select v-model="audetForm.scene" filterable placeholder="请选择" style="width: 100%">
<el-option label="彻底删除" value="delete" />
<el-option label="恢复至公海" value="report" />
</el-select>
</el-form-item>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<div style="display: flex; justify-content: flex-end; margin-top: 24px">
<el-button @click="audetDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAudetSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
<!-- 图片预览 -->
<el-image-viewer
v-if="imageViewerVisible"
:url-list="imageViewerUrls"
:initial-index="imageViewerIndex"
@close="imageViewerVisible = false"
/>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { useAuth } from '@/composables/useAuth'
import { manageService } from '@/api/adminApi'
defineOptions({ name: 'AdminCustomerAudit' })
const { hasAuth } = useAuth()
const keyword = ref<string>('')
const loading = ref(false)
const detailDialogVisible = ref(false)
const audetDialogVisible = ref(false)
const detailRow = ref<any>(null)
// 回显的无效原因与附件
const reportRemark = ref<string>('')
const reportAttachments = ref<Array<{ name: string; url: string }>>([])
const imageViewerVisible = ref(false)
const imageViewerUrls = ref<string[]>([])
const imageViewerIndex = ref(0)
// 规范化 URL:去除前缀 @,清理空白
const normalizeUrl = (url: string): string => {
return (url || '').trim().replace(/^@+/, '')
}
// 从 URL 提取文件名(忽略查询参数)
const fileNameFromUrl = (url: string): string => {
const u = normalizeUrl(url)
try {
const parsed = new URL(u)
const name = parsed.pathname.split('/').pop() || ''
return decodeURIComponent(name)
} catch (_) {
const path = u.split('?')[0]
const name = path.split('/').pop() || u
try {
return decodeURIComponent(name)
} catch {
return name
}
}
}
const pagination = reactive({
currentPage: 1,
pageSize: 10,
@@ -163,8 +268,53 @@
//审核
const goaudit = async (row: any) => {
audetForm.id = row.id
// 保存行数据用于展示
detailRow.value = row
// 兼容字段:优先 remark,其次 report_desc/desc
reportRemark.value = row.audit?.remark || ''
// 映射为可用的 {name, url}
reportAttachments.value = (row.audit?.img || [])
.map((it: any) => {
if (typeof it === 'string') {
const url = normalizeUrl(it)
return { name: fileNameFromUrl(url) || url, url }
}
const url = normalizeUrl(it.storage_path_full || it.url || it.file_url || '')
const name = it.original_name || it.name || it.file_name || fileNameFromUrl(url) || ''
return { name, url }
})
.filter((x: { url: any }) => x.url)
// 仅图片列表用于预览
imageViewerUrls.value = reportAttachments.value
.map((x) => normalizeUrl(x.url))
.filter((u) => /\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(u))
audetDialogVisible.value = true
}
const openImagePreview = (url: string) => {
const nurl = normalizeUrl(url)
// 按当前附件重建预览列表(确保与点击地址一致且包含查询参数的图片)
const imgs = reportAttachments.value
.map((x) => normalizeUrl(x.url))
.filter((u) => /\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(u))
imageViewerUrls.value = imgs.length > 0 ? imgs : [nurl]
const idx = imageViewerUrls.value.findIndex((u) => u === nurl)
imageViewerIndex.value = idx >= 0 ? idx : 0
imageViewerVisible.value = true
}
const downloadAttachment = (att: { name: string; url: string }) => {
try {
const a = document.createElement('a')
a.href = att.url
a.download = att.name || '附件'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
} catch (e) {
console.error('下载失败', e)
}
}
const handleAudetSubmit = async () => {
const res = await manageService.crmCustomerAudit(audetForm)
ElMessage.success(res?.message || '设置成功!')
+21 -3
View File
@@ -32,14 +32,24 @@
>
<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="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="email" label="邮箱" align="center" /> -->
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
@@ -49,6 +59,13 @@
</template>
</el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column
prop="code"
label="编号"
align="center"
min-width="120px"
show-overflow-tooltip
/>
<!-- <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">
@@ -240,6 +257,7 @@
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import emptyimg from '@/assets/img/crm/empty.png'
defineOptions({ name: 'AdminHighseas' })
const TIMELINE_PLACEMENT = 'top'
const activecolor = '#3471ff'
const { hasAuth } = useAuth()
+53 -8
View File
@@ -108,14 +108,24 @@
>
<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="contacts" label="客户名称" align="center" min-width="120px" />
<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="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="email" label="邮箱" align="center" /> -->
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
@@ -126,7 +136,15 @@
</el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column prop="user_name" label="跟单人"></el-table-column>
<el-table-column prop="follow_last" label="上次跟进"></el-table-column>
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
<el-table-column
prop="code"
label="编号"
align="center"
min-width="120px"
show-overflow-tooltip
/>
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
@@ -157,6 +175,15 @@
>
<i class="iconfont-sys"> &#xe64e;</i>跟进记录
</el-link>
<el-link
type="danger"
underline="never"
style="margin-right: 8px"
@click="delectfn(scope.row)"
v-if="hasAuth('customer@delete')"
>
<i class="iconfont-sys">&#xe6d4;</i>删除
</el-link>
</template>
</el-table-column>
</template>
@@ -195,8 +222,11 @@
跟进状态:<span>{{ detailRow.follow_txt }}</span>
</div>
</div>
<div class="row">
<div>备注:{{ detailRow.describe }}</div>
</div>
</div>
<div class="dtitle">详情内容</div>
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<!-- 转移跟单对话框 -->
@@ -247,7 +277,6 @@
</div>
</el-form>
</el-dialog>
<!-- 跟进记录对话框 -->
<el-dialog v-model="followRecordDialogVisible" title="跟进记录" width="800px">
<!-- 跟进记录列表 -->
@@ -280,7 +309,7 @@
class="timeline-img"
alt="图片"
@click="handleImageClick(item.picture_full)"
style="cursor: pointer;"
style="cursor: pointer"
/>
</div>
</div>
@@ -297,7 +326,6 @@
</ElScrollbar>
</div>
</el-dialog>
<!-- 图片预览对话框 -->
<el-image-viewer
v-if="imageViewerVisible"
@@ -313,6 +341,7 @@
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import emptyimg from '@/assets/img/crm/empty.png'
defineOptions({ name: 'AdminCustomer' })
const TIMELINE_PLACEMENT = 'top'
const activecolor = '#3471ff'
const { hasAuth } = useAuth()
@@ -536,6 +565,22 @@
plzyDialogVisible.value = false
gettableList()
}
const delectfn = (row: any) => {
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'list' })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
</script>
<style scoped lang="scss">
+574
View File
@@ -0,0 +1,574 @@
<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>
<!-- <el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
<i class="iconfont-sys">&#xe89f;</i> 导出
</el-button> -->
</div>
<div class="boxshadow">
<!-- 表格 -->
<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="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="follow" label="联系方式" width="240px" show-overflow-tooltip>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<el-table-column prop="grade_txt" label="等级" align="center" />
<!-- <el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.follow)">
{{ scope.row.follow_txt }}
</span>
</template>
</el-table-column> -->
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column prop="user_name" label="跟单人"></el-table-column>
<el-table-column prop="del_source" label="来源"></el-table-column>
<el-table-column prop="follow_last" label="上次跟进"></el-table-column>
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
<el-table-column
prop="code"
label="编号"
align="center"
min-width="120px"
show-overflow-tooltip
/>
<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('recycle@detail')"
>
<i class="iconfont-sys"> &#xe70f;</i>详情
</el-link>
<el-link
type="primary"
underline="never"
style="margin-right: 8px"
@click="recoverfn(scope.row)"
v-if="hasAuth('recycle@recover')"
>
<i class="iconfont-sys"> &#xe794;</i>恢复
</el-link>
<el-link
type="danger"
underline="never"
style="margin-right: 8px"
@click="delectfn(scope.row)"
v-if="hasAuth('recycle@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.user_name }}</div>
<div>录单时间:{{ detailRow.created_at }}</div>
</div>
<div class="row">
<div>
等级:
<span>
{{ detailRow.magnitude_txt }}
</span>
</div>
<div>
状态:<span>{{ detailRow.grade_txt }}</span>
</div>
<div>
跟进状态:<span>{{ detailRow.follow_txt }}</span>
</div>
</div>
<div class="row">
<div>备注:{{ detailRow.describe }}</div>
</div>
</div>
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></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, watch } from 'vue'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'AdminRecycle' })
const { hasAuth } = useAuth()
const selectValue = ref<string[]>([])
const loading = ref(false)
const detailDialogVisible = ref(false)
const zyDialogVisible = ref(false)
const detailRow = ref<any>(null)
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 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()
})
// tabs 切换联动
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: 'recyclea'
}
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 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 recoverfn = (row: any) => {
ElMessageBox.confirm('确定要恢复此条数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await manageService.customerRestore({ id: row.id })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
const delectfn = (row: any) => {
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'force' })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
const closeImageViewer = () => {
imageViewerVisible.value = false
imageList.value = []
imageViewerIndex.value = 0
}
// 选中的行数据
const selectedRows = ref<any[]>([])
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
</script>
<style scoped lang="scss">
.timeline-img {
width: 180px;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
:deep(.el-badge) {
--el-badge-size: 16px;
}
.itemtag {
margin-top: -16px;
margin-left: -13px;
}
:deep(.el-table__header th) {
background: #f7f9fa !important;
color: #333;
font-weight: bold;
}
:deep(.el-table__empty-block) {
height: 450px !important;
}
:deep(.el-table__body tr.custom-table-row td) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__body tr td:last-child) {
border-right: none !important;
}
:deep(.el-table__body tr:last-child td) {
border-bottom: none !important;
}
:deep(.el-table__header-wrapper th) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__header-wrapper th:last-child) {
border-right: none !important;
}
:deep(.el-table) {
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.customer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.customer-search-group {
display: flex;
gap: 12px;
}
.customer-tags {
background: var(--art-main-bg-color);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.filter-label {
width: 120px;
color: #666;
margin-right: 8px;
flex-shrink: 0;
font-size: 14px;
}
.filter-tags {
flex: 1;
color: #333;
font-weight: 500;
font-size: 14px;
}
.customer-tabs {
margin-bottom: 8px;
}
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.customer-table {
background: var(--art-main-bg-color);
border-radius: 8px;
margin-top: 0;
}
.filter-more-btn {
text-align: center;
margin-top: 8px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.tag-items {
cursor: pointer;
border-radius: 4px;
padding: 5px 10px;
transition: background 0.2s;
min-width: 100px;
display: inline-block;
margin-right: 30px;
}
.tag-item {
border-radius: 4px;
padding: 5px 10px;
}
.tag-itemall {
border-radius: 4px;
padding: 5px 10px;
margin-right: 20px;
}
.tag-active {
background: #3471ff;
color: #fff;
}
.tag-active :deep(.el-badge__content) {
color: #3471ff !important; /* 文字颜色 */
background-color: #fff !important; /* 背景色 */
}
.detail-info-table {
padding-left: 15px;
.row {
display: flex;
justify-content: flex-start;
margin-bottom: 6px;
> div {
flex: 1;
min-width: 0;
margin-right: 16px;
overflow: hidden;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.dtitle {
line-height: 40px;
color: #333;
font-size: 16px;
font-weight: bold;
padding-left: 15px;
}
.detils-content {
padding-left: 15px;
img {
max-width: 100% !important;
}
}
.follow-record-header {
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
margin-bottom: 15px;
.customer-info {
display: flex;
align-items: center;
.label {
font-size: 14px;
color: #606266;
margin-right: 5px;
}
.value {
font-size: 14px;
color: #303133;
font-weight: bold;
}
}
}
.follow-record-list {
padding: 15px;
max-height: 500px;
overflow-y: auto;
.list-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 15px;
padding-left: 10px;
}
.record-item {
background-color: #f5f7fa;
border-radius: 4px;
padding: 10px 15px;
margin-bottom: 10px;
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.record-type {
font-size: 14px;
color: #3471ff;
font-weight: bold;
background-color: #e6f7ff;
padding: 2px 8px;
border-radius: 4px;
}
.record-time {
font-size: 14px;
color: #909399;
}
.record-user {
font-size: 14px;
color: #3471ff;
font-weight: bold;
}
}
.record-content {
font-size: 14px;
color: #303133;
line-height: 1.6;
margin-bottom: 5px;
}
.record-footer {
.next-follow {
font-size: 14px;
color: #909399;
}
}
}
.no-records {
text-align: center;
color: #909399;
padding: 20px 0;
}
}
</style>
+389
View File
@@ -0,0 +1,389 @@
<template>
<div class="account-page">
<div class="account-toolbar boxshadow">
<div class="account-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 180px"
v-model="keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getAccountList"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getAccountList" v-ripple>搜索</el-button>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('account@add')" type="primary"
>新增账户</ElButton
>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="账户简称" prop="short_desc" min-width="160" />
<ElTableColumn label="账户名称" prop="account_name" min-width="160" />
<ElTableColumn label="账户号码" prop="account_number" min-width="160" />
<ElTableColumn label="银行名称" prop="bank_name" min-width="140" />
<!-- <ElTableColumn label="开户支行" prop="bank_branch" min-width="160" /> -->
<ElTableColumn label="公司名称" prop="corp_name" min-width="160" />
<ElTableColumn label="货币" prop="currency_id" min-width="120">
<template #default="scope">
<span>{{ getCurrencyLabel(scope.row.currency_id) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="排序" prop="sort" />
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
scope.row.status ? '启用' : '停用'
}}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" min-width="160px" align="center">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('account@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteAccount(scope.row)"
v-if="hasAuth('account@destroy')"
>删除</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增账户' : '编辑账户'"
align-center
width="700px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="form">
<ElFormItem label="账户简称" prop="short_desc">
<ElInput v-model="form.short_desc" placeholder="请输入账户简称" />
</ElFormItem>
<ElFormItem label="账户名称" prop="account_name">
<ElInput v-model="form.account_name" placeholder="请输入账户名称" />
</ElFormItem>
<ElFormItem label="账户号码" prop="account_number">
<ElInput v-model="form.account_number" placeholder="请输入账户号码" />
</ElFormItem>
<ElFormItem label="银行名称" prop="bank_name">
<ElInput v-model="form.bank_name" placeholder="请输入银行名称" />
</ElFormItem>
<!-- <ElFormItem label="开户支行" prop="bank_branch">
<ElInput v-model="form.bank_branch" placeholder="请输入开户支行" />
</ElFormItem> -->
<ElFormItem label="公司名称" prop="corp_id">
<el-select v-model="form.corp_id" placeholder="请选择公司" style="width: 100%">
<el-option
v-for="account in companyList"
:key="account.id"
:label="account.name"
:value="account.id"
/>
</el-select>
</ElFormItem>
<ElFormItem label="账户货币类型" prop="currency_id">
<el-select v-model="form.currency_id" placeholder="请选择货币" style="width: 100%">
<el-option
v-for="opt in currencyOptions"
:key="opt.id"
:label="opt.currency_name + ' (' + opt.currency_code + ')'"
:value="opt.id"
/>
</el-select>
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
<ElFormItem label="账户描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
maxlength="300"
show-word-limit
placeholder="请输入账户描述"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { financeService } from '@/api/financeApi'
import { commonService } from '@/api/commonApi'
import { useAuth } from '@/composables/useAuth'
import { UserService } from '@/api/usersApi'
const { hasAuth } = useAuth()
defineOptions({ name: 'FinanceAccount' })
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const currencyFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
// 下拉:货币
const currencyOptions = ref<any[]>([])
const loadCurrencyOptions = async () => {
try {
const res = await commonService.getCurrencySelect()
currencyOptions.value = res || []
} catch (e) {
currencyOptions.value = []
}
}
const companyList = ref<any>()
const getcompanyList = async () => {
const response = await UserService.getCompanyList()
companyList.value = response || []
}
const form = reactive<any>({
id: '',
currency_id: undefined,
short_desc: '',
status: 1,
sort: 0,
bank_name: '',
bank_branch: '',
description: '',
account_number: '',
account_name: '',
corp_id: ''
})
const rules = reactive<FormRules>({
short_desc: [{ required: true, message: '请输入账户简称', trigger: 'blur' }],
account_name: [{ required: true, message: '请输入账户名称', trigger: 'blur' }],
account_number: [{ required: true, message: '请输入账户号码', trigger: 'blur' }]
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableData = ref<any[]>([])
const tableRef = ref()
onMounted(() => {
getcompanyList()
loadCurrencyOptions()
getAccountList()
})
watch([keyword, currencyFilter, statusFilter], () => {
pagination.currentPage = 1
getAccountList()
})
const getAccountList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await financeService.getAccountList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取账户列表失败:', error)
ElMessage.error('获取账户列表失败')
} finally {
loading.value = false
}
}
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getAccountList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getAccountList()
}
const showDialog = (type: 'add' | 'edit', row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
Object.assign(form, {
id: row.id,
short_desc: row.short_desc,
currency_id: row.currency_id,
status: row.status,
sort: row.sort ?? 0,
bank_name: row.bank_name,
bank_branch: row.bank_branch,
description: row.description,
account_number: row.account_number,
account_name: row.account_name,
corp_id: row.corp_id
})
} else {
Object.assign(form, {
id: '',
short_desc: '',
currency_id: undefined,
status: 1,
sort: 0,
bank_name: '',
bank_branch: '',
description: '',
account_number: '',
account_name: '',
corp_id: ''
})
}
}
const deleteAccount = (row: any) => {
ElMessageBox.confirm('确定删除该账户吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await financeService.accountDelete({ id: row.id })
ElMessage.success(message)
getAccountList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
const { message } = await financeService.accountAdd({
currency_id: form.currency_id,
short_desc: form.short_desc,
status: form.status,
sort: form.sort,
bank_name: form.bank_name,
bank_branch: form.bank_branch,
description: form.description,
account_number: form.account_number,
account_name: form.account_name,
corp_id: form.corp_id
})
ElMessage.success(message)
} else {
const { message } = await financeService.accountEdit({
id: form.id,
currency_id: form.currency_id,
short_desc: form.short_desc,
status: form.status,
sort: form.sort,
bank_name: form.bank_name,
bank_branch: form.bank_branch,
description: form.description,
account_number: form.account_number,
account_name: form.account_name,
corp_id: form.corp_id
})
ElMessage.success(message)
}
dialogVisible.value = false
getAccountList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
const getCurrencyLabel = (id: number | undefined) => {
if (!id) return '-'
const found = currencyOptions.value.find((c: any) => c.id === id)
return found ? `${found.currency_name} (${found.currency_code})` : id
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.account-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.account-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.account-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+389
View File
@@ -0,0 +1,389 @@
<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">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('currency@add')"
>新增</ElButton
>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="货币编码" prop="currency_code" min-width="140" />
<ElTableColumn label="货币名称" prop="currency_name" min-width="140" />
<ElTableColumn label="符号" prop="currency_symbol" width="100" />
<ElTableColumn label="当前汇率" prop="exchange_rate" min-width="120" />
<ElTableColumn label="允许上浮动%" prop="float_percent_up" min-width="120" />
<ElTableColumn label="允许下浮动%" prop="float_percent_down" min-width="120" />
<ElTableColumn label="状态" prop="status" width="120">
<template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'">
{{ scope.row.status ? '启用' : '停用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="240px">
<template #default="scope">
<ElRow>
<el-button link @click="showDetail(scope.row)" v-if="hasAuth('currency@show')">
详情
</el-button>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('currency@update')"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
v-if="hasAuth('currency@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增货币' : '编辑货币'"
align-center
width="640px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px" class="form">
<ElFormItem label="货币编码" prop="currency_code">
<ElInput v-model="form.currency_code" />
</ElFormItem>
<ElFormItem label="货币名称" prop="currency_name">
<ElInput v-model="form.currency_name" />
</ElFormItem>
<ElFormItem label="货币符号" prop="currency_symbol">
<ElInput v-model="form.currency_symbol" />
</ElFormItem>
<ElFormItem label="当前汇率" prop="exchange_rate">
<ElInputNumber
v-model="form.exchange_rate"
:min="0"
:step="0.0001"
:precision="6"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="允许上浮动%" prop="float_percent_up">
<ElInputNumber
v-model="form.float_percent_up"
:min="0"
:max="100"
:step="0.01"
:precision="2"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="允许下浮动%" prop="float_percent_down">
<ElInputNumber
v-model="form.float_percent_down"
:min="0"
:max="100"
:step="0.01"
:precision="2"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<ElDialog v-model="detailVisible" title="货币详情" align-center width="640px">
<div class="detail-dialog">
<div class="section">
<div class="info-grid">
<div class="label">货币编码</div>
<div class="value">{{ detailData.currency_code || '-' }}</div>
</div>
<div class="info-grid">
<div class="label">货币名称</div>
<div class="value">{{ detailData.currency_name || '-' }}</div>
</div>
<div class="info-grid">
<div class="label">货币符号</div>
<div class="value">{{ detailData.currency_symbol || '-' }}</div>
</div>
<div class="info-grid">
<div class="label">当前汇率</div>
<div class="value">{{ detailData.exchange_rate ?? '-' }}</div>
</div>
<div class="info-grid">
<div class="label">允许上浮动%</div>
<div class="value">{{ detailData.float_percent_up ?? '-' }}</div>
</div>
<div class="info-grid">
<div class="label">允许下浮动%</div>
<div class="value">{{ detailData.float_percent_down ?? '-' }}</div>
</div>
<div class="info-grid">
<div class="label">状态</div>
<div class="value">{{ detailData.status ? '启用' : '停用' }}</div>
</div>
<div class="info-grid">
<div class="label">创建时间</div>
<div class="value">{{ detailData.created_at || '-' }}</div>
</div>
<div class="info-grid">
<div class="label">更新时间</div>
<div class="value">{{ detailData.updated_at || '-' }}</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="detailVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { financeService } from '@/api/financeApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'TypeCurrency' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const rules = reactive<FormRules>({
currency_code: [{ required: true, message: '请输入货币编码', trigger: 'blur' }],
currency_name: [{ required: true, message: '请输入货币名称', trigger: 'blur' }],
exchange_rate: [{ required: true, message: '请输入当前汇率', trigger: 'change' }]
})
const form = reactive<any>({
id: '',
currency_code: '',
currency_name: '',
currency_symbol: '',
exchange_rate: 0,
float_percent_up: 0,
float_percent_down: 0,
status: 1
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getCurrencyList()
})
watch([keyword], () => {
pagination.currentPage = 1
getCurrencyList()
})
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getCurrencyList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getCurrencyList()
}
// 获取货币列表
const getCurrencyList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await financeService.getCurrencyList({
current: currentPage,
size: pageSize,
keyword: keyword.value
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const dialogType = ref('add')
const detailVisible = ref(false)
const detailData = ref<any>({})
const showDialog = (type: 'add' | 'edit', row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
Object.assign(form, {
id: row.id,
currency_code: row.currency_code,
currency_name: row.currency_name,
currency_symbol: row.currency_symbol,
exchange_rate: row.exchange_rate ?? 0,
float_percent_up: row.float_percent_up ?? 0,
float_percent_down: row.float_percent_down ?? 0,
status: row.status ?? 1
})
} else {
Object.assign(form, {
id: '',
currency_code: '',
currency_name: '',
currency_symbol: '',
exchange_rate: 0,
float_percent_up: 0,
float_percent_down: 0,
status: 1
})
}
}
const handleDelete = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await financeService.currencyDelete({ id: row.id })
ElMessage.success(message)
getCurrencyList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
if (dialogType.value === 'add') {
const { id, ...payload } = form
const { message } = await financeService.currencyAdd(payload)
ElMessage.success(message)
} else {
const { message } = await financeService.currencyEdit(form)
ElMessage.success(message)
}
dialogVisible.value = false
getCurrencyList()
})
}
const showDetail = async (row: any) => {
detailVisible.value = true
const res = await financeService.currencyDetail({ id: row.id })
detailData.value = res || {}
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.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;
}
.message-text {
padding: 10px 14px;
line-height: 1.7;
white-space: pre-line;
word-break: break-word;
}
.detail-dialog {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 12px 14px;
}
.info-grid {
display: grid;
grid-template-columns: 96px 1fr;
gap: 6px 12px;
align-items: center;
padding: 6px 0;
}
.label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.value {
color: var(--el-text-color-primary);
font-size: 14px;
}
.content-box {
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
max-height: 320px;
overflow: auto;
}
</style>
+1
View File
@@ -349,6 +349,7 @@
import { CrmService } from '@/api/crmApi'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'InquirySystem' })
const { hasAuth } = useAuth()
const loading = ref(false)
const dialogType = ref('add')
+339
View File
@@ -0,0 +1,339 @@
<template>
<div class="color-page">
<div class="color-toolbar boxshadow">
<div class="color-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getannualList"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getannualList" v-ripple>搜索</el-button>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton
@click="showDialog('add')"
v-ripple
v-if="hasAuth('kpi_annual@add')"
type="primary"
>
新增
</ElButton>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="公司" prop="corp_id">
<template #default="scope">
<p> {{ scope.row.corp_name ? scope.row.corp_name : '' }}</p>
</template>
</ElTableColumn>
<ElTableColumn label="年份" prop="year" />
<ElTableColumn label="业务员" prop="user_name" />
<ElTableColumn label="年度目标数" prop="annual_target" />
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" width="120">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('kpi_annual@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteColor(scope.row)"
v-if="hasAuth('kpi_annual@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
align-center
width="600px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<!-- <ElFormItem label="公司" prop="corp_id">
<ElSelect v-model="form.corp_id" placeholder="请选择公司">
<ElOption
v-for="item in companyList"
:key="item.id"
:value="item.id"
:label="item.name"
></ElOption>
</ElSelect>
</ElFormItem> -->
<ElFormItem label="年份" prop="year">
<ElInputNumber v-model="form.year" :min="1970" :max="2999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="业务员" prop="user_id">
<ElSelect v-model="form.user_id" filterable placeholder="请选择业务员">
<el-option
v-for="item in companyUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
<el-text class="mx-1" type="warning">不选业务员表示公司年度总目标</el-text>
</ElFormItem>
<ElFormItem label="年度目标数" prop="annual_target">
<ElInputNumber v-model="form.annual_target" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { kpiService } from '@/api/admin/kpi'
import { useAuth } from '@/composables/useAuth'
import { UserService } from '@/api/usersApi'
import { manageService } from '@/api/adminApi'
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminKpiAnnual' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
// 获取公司列表
const companyList = ref<any>()
const getCompanyList = async () => {
const response = await UserService.getCompanyList()
companyList.value = response || []
}
const companyUserList = ref<any>()
// 获取用户列表
const getCompanyUserList = async () => {
const response = await manageService.presetusers()
companyUserList.value = response || []
}
// 表单数据
const form = reactive<any>({
id: '',
year: '',
user_id: '',
annual_target: 0
})
// 表单验证规则
const rules = reactive<FormRules>({
year: [{ required: true, message: '请输入年份', trigger: 'blur' }],
annual_target: [{ required: true, message: '请输入年度目标数', trigger: 'blur' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getannualList()
})
// 监听搜索条件变化
watch([keyword], () => {
pagination.currentPage = 1
getannualList()
})
// 获取颜色列表
const getannualList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await kpiService.getyearlylist(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取颜色列表失败:', error)
ElMessage.error('获取颜色列表失败')
} finally {
loading.value = false
}
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getannualList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getannualList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
getCompanyList()
getCompanyUserList()
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式
Object.assign(form, {
id: row.id,
year: row.year,
user_id: row.user_id,
annual_target: row.annual_target
})
} else {
// 新增模式
Object.assign(form, {
id: '',
year: '',
user_id: '',
annual_target: 0
})
}
}
// 删除年度目标
const deleteColor = (row: any) => {
ElMessageBox.confirm('确定删除该年度目标吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await kpiService.deleteyearly({ id: row.id })
ElMessage.success(message)
getannualList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
// 新增
const { message } = await kpiService.addyearly({
year: form.year,
user_id: form.user_id,
annual_target: form.annual_target
})
ElMessage.success(message)
} else {
// 编辑
const { message } = await kpiService.updateyearly({
id: form.id,
year: form.year,
user_id: form.user_id,
annual_target: form.annual_target
})
ElMessage.success(message)
}
dialogVisible.value = false
getannualList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.color-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.color-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.color-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+331
View File
@@ -0,0 +1,331 @@
<template>
<div class="color-page">
<div class="color-toolbar boxshadow">
<div class="color-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getMonthlyList"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getMonthlyList" v-ripple>搜索</el-button>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton
@click="showDialog('add')"
v-ripple
v-if="hasAuth('kpi_monthly@add')"
type="primary"
>
新增
</ElButton>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="用户" prop="user_name" min-width="120" />
<ElTableColumn label="年份" prop="year" min-width="120" />
<ElTableColumn label="月份" prop="month" min-width="120" />
<ElTableColumn label="本月销售数" prop="achieved" min-width="140" />
<ElTableColumn fixed="right" label="操作" width="120">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('kpi_monthly@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteMonthly(scope.row)"
v-if="hasAuth('kpi_monthly@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增月度-业绩' : '编辑月度-业绩'"
align-center
width="600px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<ElFormItem label="用户" prop="user_id">
<ElSelect v-model="form.user_id" filterable placeholder="请选择用户">
<ElOption
v-for="item in companyUserList"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="年份" prop="year">
<ElInputNumber v-model="form.year" :min="1970" :max="2999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="月份" prop="month">
<ElInputNumber v-model="form.month" :min="1" :max="12" style="width: 100%" />
</ElFormItem>
<ElFormItem label="本月销售数" prop="achieved">
<ElInputNumber v-model="form.achieved" :min="0" :max="999999" style="width: 100%" />
</ElFormItem>
<!-- 预留字段:目标数,不显示,仅占位保存 -->
<input type="hidden" v-model="form.target" />
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { kpiService } from '@/api/admin/kpi'
import { useAuth } from '@/composables/useAuth'
import { manageService } from '@/api/adminApi'
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminKpiMonthly' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const companyUserList = ref<any>()
// 获取用户列表
const getCompanyUserList = async () => {
const response = await manageService.presetusers()
companyUserList.value = response || []
}
// 表单数据
const form = reactive<any>({
id: '',
user_id: '',
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
achieved: 0,
target: undefined
})
// 表单验证规则
const rules = reactive<FormRules>({
user_id: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
year: [{ required: true, message: '请输入年份', trigger: 'change' }],
month: [{ required: true, message: '请输入月份', trigger: 'change' }],
achieved: [{ required: true, message: '请输入本月销售数', trigger: 'change' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getMonthlyList()
})
// 监听搜索条件变化
watch([keyword], () => {
pagination.currentPage = 1
getMonthlyList()
})
// 获取月度业绩列表
const getMonthlyList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await kpiService.getmonthlylist(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
ElMessage.error('获取列表失败')
} finally {
loading.value = false
}
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getMonthlyList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getMonthlyList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
getCompanyUserList()
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式
Object.assign(form, {
id: row.id,
user_id: row.user_id,
year: row.year,
month: row.month,
achieved: row.achieved,
target: row.target
})
} else {
// 新增模式
Object.assign(form, {
id: '',
user_id: '',
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
achieved: 0,
target: undefined
})
}
}
// 删除月度业绩
const deleteMonthly = (row: any) => {
ElMessageBox.confirm('确定删除该记录吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await kpiService.deletemonthly({ id: row.id })
ElMessage.success(message)
getMonthlyList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
// 新增
const { message } = await kpiService.addmonthly({
user_id: form.user_id,
year: form.year,
month: form.month,
achieved: form.achieved
})
ElMessage.success(message)
} else {
// 编辑
const { message } = await kpiService.updatemonthly({
id: form.id,
user_id: form.user_id,
year: form.year,
month: form.month,
achieved: form.achieved
})
ElMessage.success(message)
}
dialogVisible.value = false
getMonthlyList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.color-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.color-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.color-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
File diff suppressed because it is too large Load Diff
+425
View File
@@ -0,0 +1,425 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<!-- <div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div> -->
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</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"
>
<template #default>
<ElTableColumn label="备货单编号" prop="order_no" min-width="140" />
<ElTableColumn label="阿里订单号/PI号" prop="pi_no" min-width="160" />
<ElTableColumn label="客户名称" prop="customer_snapshot" min-width="120" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" min-width="120" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" min-width="100" />
<ElTableColumn label="日期" prop="created_at" min-width="120" />
<ElTableColumn label="状态" prop="approval_status">
<template #default="scope">
<div v-if="scope.row.appro_status">
<div
><span :class="statusClass(scope.row.appro_status.status)">{{ scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : '' }}</span>
<span v-if="scope.row.appro_status.name != ''"
>【{{ scope.row.appro_status.name }}】</span
>
</div>
</div>
<div v-else></div>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="120" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
type="warning"
underline="never"
v-if="scope.row.file_url != null && scope.row.file_url != ''"
@click="printfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe782;</i> 打印预览
</el-link>
<el-link type="primary" underline="never" @click="handleView1(scope.row)">
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 订单详情弹窗(使用公共内容组件) -->
<el-dialog
v-model="orderDialog.visible"
:title="orderDialog.title"
width="95%"
:close-on-click-modal="false"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
<div class="boxshadow">
<div class="form-title">
<!-- <span class="border"></span> -->
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="orderDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="download">下载</el-button>
</div>
<div
v-if="pdf.images.length"
style="overflow: auto; border: 1px solid #eee; border-radius: 6px; padding: 8px"
>
<img
v-for="(img, idx) in pdf.images"
:key="idx"
:src="img"
style="display: block; width: 100%; margin-bottom: 8px; background: #fff"
/>
</div>
<iframe
v-else-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
// import { CrmService } from '@/api/crmApi'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
import { usePdfPreview } from '@/composables/usePdfPreview'
// PDF 预览/打印(公共)
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
defineOptions({ name: 'AdminFinalListPage' })
// 订单详情弹窗
const orderDialog = reactive({
visible: false,
title: '订单详情',
orderData: {} as OrderData
})
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 1, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await manageService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
function statusClass(status: any) {
if (status === 'rejected') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
//打印
const printfn = (row: any) => {
manageService
.printStock({ id: row.id })
.then((res) => {
openWithResponse(res)
// 优先转成图片以适配不带插件的环境
prepareImages()
// 同时保留 blob 预览作为兜底
preparePreview()
})
.catch((err) => {
console.log(err)
})
}
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const handleView1 = async (row: any) => {
const res = await manageService.checkStock({ id: row.object_id })
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
if (res) {
// 获取订单详情成功
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
// router.push({
// path: '/admin/stock/final_show_admin',
// query: { id: row.id } // 可带客户名等参数
// })
// TODO: 跳转或弹窗查看备货单
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
</script>
<style scoped lang="scss">
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable { cursor: pointer; }
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
</style>
+678
View File
@@ -0,0 +1,678 @@
<template>
<div class="stocking-show-page">
<div class="header boxshadow">
<div class="header-left">
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
<div class="sub">
<span class="mr40">备货单编号:{{ header.order_no }}</span>
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain @click="printCurrent">打印整单</ElButton>
</div>
<!-- 基础信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="阿里订单号/PI号">
<ElInput v-model="base.pn_no" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="日期">
<ElInput v-model="base.date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElFormItem label="客户来源">
<ElInput v-model="base.channel" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户名称">
<ElInput v-model="base.customer_name" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户所在国">
<ElInput v-model="base.client_country" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 订单详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
<div class="items-wrap">
<div class="item-row" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12">
<ElCol :span="8">
<ElFormItem label="车型" label-width="64px">
<ElInput v-model="item.model" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/外" label-width="72px">
<ElInput v-model="item.color_out" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="内饰" label-width="52px">
<ElInput v-model="item.color_in" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="改装" label-width="52px">
<ElInput v-model="item.modify" disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
<template #append>{{ item.unit_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model="item.total_price" disabled>
<template #append>{{ item.total_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
<template #append>{{ item.helper_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
<template #append>{{ item.port_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="里程" label-width="52px">
<ElInput v-model="item.mileage" disabled />
</ElFormItem>
</ElCol>
<template v-if="(item.mileage ?? 0) <= 0">
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<template v-else>
<ElCol :span="4">
<ElFormItem label="挂牌价" label-width="55px">
<ElInput v-model="item.listing_price" disabled>
<template #append>{{ item.listing_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="二手减价" label-width="72px">
<ElInput v-model="item.reduction_price" disabled>
<template #append>{{ item.reduction_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled>
<template #append>{{ item.profit_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="车架号" label-width="70px">
<ElInput v-model.number="item.vin" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<ElRow :gutter="12">
<ElCol :span="6">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="6">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="base.contract_rate" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<!-- 运输详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog v-model="pdf.visible" title="打印预览" width="80%" top="40px" :close-on-click-modal="false">
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
<div style="display:flex; justify-content:flex-end; gap: 8px;">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex:1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { manageService } from '@/api/adminApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
defineOptions({ name: 'AdminFinalShow' })
const printCurrent = async () => {
const res = await manageService.printStock({ id: stocking_id })
openWithResponse(res)
preparePreview()
}
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const ship = reactive({
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await manageService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const listing = toMoneyView(it?.listing_price)
const reduction = toMoneyView(it?.reduction_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
mileage: it?.mileage ?? 0,
vin: it?.vin || '',
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
listing_price: listing.amount,
listing_price_currency: listing.currency,
reduction_price: reduction.amount,
reduction_price_currency: reduction.currency,
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page { min-height: 100vh; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 { margin-right: 40px; }
}
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
.form-title .title { margin-left: 6px; }
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
</style>
+670
View File
@@ -0,0 +1,670 @@
<template>
<div class="stocking-show-page">
<div class="header boxshadow">
<div class="header-left">
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
<div class="sub">
<span class="mr40">备货单编号:{{ header.order_no }}</span>
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain @click="printCurrent">打印整单</ElButton>
</div>
<!-- 基础信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="阿里订单号/PI号">
<ElInput v-model="base.pn_no" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="日期">
<ElInput v-model="base.date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElFormItem label="客户来源">
<ElInput v-model="base.channel" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户名称">
<ElInput v-model="base.customer_name" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户所在国">
<ElInput v-model="base.client_country" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 订单详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
<div class="items-wrap">
<div class="item-row" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12">
<ElCol :span="8">
<ElFormItem label="车型" label-width="64px">
<ElInput v-model="item.model" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/外" label-width="72px">
<ElInput v-model="item.color_out" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="内饰" label-width="52px">
<ElInput v-model="item.color_in" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="改装" label-width="52px">
<ElInput v-model="item.modify" disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
<template #append>{{ item.unit_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model="item.total_price" disabled>
<template #append>{{ item.total_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
<template #append>{{ item.helper_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
<template #append>{{ item.port_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="里程" label-width="52px">
<ElInput v-model="item.mileage" disabled />
</ElFormItem>
</ElCol>
<template v-if="(item.mileage ?? 0) <= 0">
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<template v-else>
<ElCol :span="4">
<ElFormItem label="挂牌价" label-width="55px">
<ElInput v-model="item.listing_price" disabled>
<template #append>{{ item.listing_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="二手减价" label-width="72px">
<ElInput v-model="item.reduction_price" disabled>
<template #append>{{ item.reduction_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled>
<template #append>{{ item.profit_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
</div>
<ElRow :gutter="12">
<ElCol :span="6">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="6">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="base.contract_rate" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<!-- 运输详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog v-model="pdf.visible" title="打印预览" width="80%" top="40px" :close-on-click-modal="false">
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
<div style="display:flex; justify-content:flex-end; gap: 8px;">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex:1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { manageService } from '@/api/adminApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
defineOptions({ name: 'AdminStockingDetail' })
const printCurrent = async () => {
const res = await manageService.printStock({ id: stocking_id })
openWithResponse(res)
preparePreview()
}
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const ship = reactive({
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await manageService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
mileage: it?.mileage ?? 0,
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
listing_price: it?.listing_price?.amount ?? '',
listing_price_currency: it?.listing_price?.currency ?? '',
reduction_price: it?.reduction_price?.amount ?? '',
reduction_price_currency: it?.reduction_price?.currency ?? '',
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page { min-height: 100vh; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 { margin-right: 40px; }
}
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
.form-title .title { margin-left: 6px; }
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
</style>
@@ -0,0 +1,469 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<!-- <div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div> -->
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</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"
>
<template #default>
<ElTableColumn label="备货单编号" prop="order_no" />
<ElTableColumn label="阿里订单号/PI号" prop="pi_no" />
<ElTableColumn label="客户名称" prop="customer_snapshot" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" />
<ElTableColumn label="日期" prop="created_at" />
<!-- <ElTableColumn label="补充" prop="remark" /> -->
<ElTableColumn label="状态" prop="approval_status">
<template #default="scope">
<div v-if="scope.row.appro_status">
<div
><span :class="statusClass(scope.row.appro_status.status)">{{ scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : '' }}</span>
<span v-if="scope.row.appro_status.name != ''"
>【{{ scope.row.appro_status.name }}】</span
>
</div>
</div>
<div v-else></div>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="120" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
type="warning"
underline="never"
v-if="scope.row.file_url != null && scope.row.file_url != ''"
@click="printfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe782;</i> 打印预览
</el-link>
<el-link
type="success"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看
</el-link>
<el-link
v-if="scope.row.is_use == 1"
type="success"
underline="never"
@click="handleView1(scope.row)"
>
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px;">
<el-button @click="download">下载</el-button>
</div>
<div
v-if="pdf.images.length"
style="overflow: auto; border: 1px solid #eee; border-radius: 6px; padding: 8px"
>
<img
v-for="(img, idx) in pdf.images"
:key="idx"
:src="img"
style="display: block; width: 100%; margin-bottom: 8px; background: #fff"
/>
</div>
<iframe
v-else-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
</el-dialog>
<!-- 订单详情弹窗(使用公共内容组件) -->
<el-dialog
v-model="orderDialog.visible"
:title="orderDialog.title"
width="95%"
:close-on-click-modal="false"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
<div class="boxshadow">
<div class="form-title">
<!-- <span class="border"></span> -->
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="orderDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { manageService } from '@/api/adminApi'
import { usePdfPreview } from '@/composables/usePdfPreview'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'StockingListAdmin' })
// import { useAuth } from '@/composables/useAuth'
// const { hasAuth } = useAuth()
// 订单详情弹窗
const orderDialog = reactive({
visible: false,
title: '订单详情',
orderData: {} as OrderData
})
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// PDF 预览/打印(公共)
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
onMounted(() => {
getList()
})
function statusClass(status: any) {
if (status === 'rejected') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await manageService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const handleView = async (row: any) => {
try {
// 获取订单详情数据
const res = await manageService.checkStock({ id: row.id })
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
if (res) {
// 转换数据格式以适配 OrderDetailDialog
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
} catch (error) {
console.error('获取订单详情失败:', error)
ElMessage.error('获取订单详情失败')
}
// router.push({
// path: '/admin/stock/show_admin',
// query: { id: row.id } // 可带客户名等参数
// })
// TODO: 跳转或弹窗查看备货单
}
const handleView1 = async (row: any) => {
const res = await manageService.checkStock({ id: row.object_id })
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
if (res) {
// 获取订单详情成功
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
// router.push({
// path: '/admin/stock/final_show_admin',
// query: { id: row.object_id } // 可带客户名等参数
// })
// router.push('/crmpc/stocking/final_show')
// TODO: 跳转或弹窗查看备货单
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
//打印
const printfn = (row: any) => {
manageService
.printStock({ id: row.id })
.then((res) => {
openWithResponse(res)
// 优先转成图片以适配不带插件的环境
prepareImages()
// 同时保留 blob 预览作为兜底
preparePreview()
})
.catch((err) => {
console.log(err)
})
}
</script>
<style scoped lang="scss">
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable { cursor: pointer; }
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
</style>
+12 -2
View File
@@ -15,6 +15,8 @@
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="ID" prop="id" />
@@ -91,7 +93,7 @@
import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' })
defineOptions({ name: 'Company' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
@@ -123,7 +125,15 @@
onMounted(() => {
getWorkList()
})
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 获取列表数据
const getWorkList = async () => {
loading.value = true
+538
View File
@@ -0,0 +1,538 @@
<template>
<div class="page-content">
<div v-if="!isMobile" class="dict-layout">
<ElRow :gutter="16">
<ElCol :xs="24" :sm="8" :lg="8">
<div class="dict-type-card">
<div class="card-header">
<ElSelect v-model="category_id" placeholder="请选择字典分类" clearable>
<ElOption
v-for="item in DictTypeSelect"
:key="item.id"
:label="item.item_label"
:value="item.id"
/>
</ElSelect>
<el-input
style="margin-left: 8px"
v-model="typeKeyword"
@keyup.enter="getTypeList"
placeholder="请输入关键字"
/>
<ElButton style="margin-left: 8px; min-width: 70px" @click="getTypeList" v-ripple
>搜索</ElButton
>
<ElButton
type="primary"
style="margin-left: 8px"
@click="showTypeDialog('add')"
v-if="hasAuth('system@dictionary_add')"
>新增类型</ElButton
>
</div>
<el-table
:data="typeTableData"
v-loading="typeLoading"
height="calc(100vh - 280px)"
@row-click="handleTypeRowClick"
:highlight-current-row="true"
:row-key="'id'"
>
<el-table-column prop="dict_code" label="字典编码" />
<el-table-column prop="dict_name" label="字典名称" />
<el-table-column prop="category" label="字典分类" />
<el-table-column prop="description" label="描述" show-overflow-tooltip />
<el-table-column fixed="right" label="操作" min-width="140" align="center">
<template #default="scope">
<el-button
link
size="small"
@click.stop="showTypeDialog('edit', scope.row)"
v-if="hasAuth('system@dictionary_update')"
>编辑</el-button
>
<el-button
link
type="danger"
size="small"
@click.stop="handleTypeDelete(scope.row)"
v-if="hasAuth('system@dictionary_destroy')"
>删除</el-button
>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无类型" v-show="!typeLoading" />
</template>
</el-table>
<div class="type-pagination">
<el-pagination
background
layout="total, sizes, prev, pager, next"
:current-page="typePagination.currentPage"
:page-size="typePagination.pageSize"
:total="typePagination.total"
:page-sizes="[10, 20, 30, 50]"
@current-change="handleTypeCurrentChange"
@size-change="handleTypeSizeChange"
/>
</div>
</div>
</ElCol>
<ElCol :xs="24" :sm="16" :lg="16">
<div class="dict-item-card">
<div class="card-header">
<div class="left">
<span v-if="activeType"
>当前类型:{{ activeType.dict_name }}({{ activeType.dict_code }})</span
>
<span v-else class="muted">请选择左侧字典类型</span>
</div>
<div class="right">
<ElInput
v-model="itemKeyword"
placeholder="搜索字典项名称/值"
clearable
style="width: 240px"
@keyup.enter="getItemList"
/>
<ElButton style="margin-left: 8px" @click="getItemList" v-ripple>搜索</ElButton>
<ElButton
type="primary"
style="margin-left: 8px"
@click="showItemDialog('add')"
:disabled="!activeType"
v-if="hasAuth('system@dictionary_add')"
>新增字典项</ElButton
>
</div>
</div>
<ArtTable
ref="itemTableRef"
row-key="id"
:loading="itemLoading"
:data="itemTableData"
:currentPage="itemPagination.currentPage"
:pageSize="itemPagination.pageSize"
:total="itemPagination.total"
@size-change="handleItemSizeChange"
@current-change="handleItemCurrentChange"
>
<template #default>
<ElTableColumn label="ID" prop="id" width="80" />
<ElTableColumn label="字典项名称" prop="item_label" min-width="160" />
<ElTableColumn
label="字典项值"
prop="item_value"
min-width="160"
show-overflow-tooltip
/>
<ElTableColumn label="备注" prop="remark" min-width="160" show-overflow-tooltip />
<!-- <ElTableColumn label="排序" prop="sort" width="100" /> -->
<ElTableColumn label="状态" prop="status" width="120">
<template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'">{{
scope.row.status ? '启用' : '禁用'
}}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" min-width="140px" align="center">
<template #default="scope">
<ElRow>
<el-button
link
@click="showItemDialog('edit', scope.row)"
v-if="hasAuth('system@dictionary_update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="handleItemDelete(scope.row)"
v-if="hasAuth('system@dictionary_destroy')"
>删除</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
</ElCol>
</ElRow>
<ElDialog
v-model="typeDialogVisible"
:title="typeDialogType === 'add' ? '新增字典类型' : '编辑字典类型'"
width="40%"
align-center
>
<ElForm ref="typeFormRef" :model="typeForm" :rules="typeRules" label-width="100px">
<ElFormItem label="字典分类" prop="category_id">
<ElSelect v-model="typeForm.category_id" placeholder="请选择字典分类" clearable>
<ElOption
v-for="item in DictTypeSelect"
:key="item.id"
:label="item.item_label"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="字典编码" prop="dict_code">
<ElInput v-model="typeForm.dict_code" />
</ElFormItem>
<ElFormItem label="字典名称" prop="dict_name">
<ElInput v-model="typeForm.dict_name" />
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput v-model="typeForm.description" type="textarea" :rows="2" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="typeForm.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="typeDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleTypeSubmit(typeFormRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="itemDialogVisible"
:title="itemDialogType === 'add' ? '新增字典项' : '编辑字典项'"
width="50%"
align-center
>
<ElForm ref="itemFormRef" :model="itemForm" :rules="itemRules" label-width="100px">
<ElFormItem label="所属类型">
<ElInput
:model-value="activeType ? `${activeType.dict_name}(${activeType.dict_code})` : ''"
disabled
/>
</ElFormItem>
<ElFormItem label="字典项名称" prop="item_label">
<ElInput v-model="itemForm.item_label" />
</ElFormItem>
<ElFormItem label="字典项值" prop="item_value">
<ElInput v-model="itemForm.item_value" type="textarea" :rows="3" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="itemForm.sort" :min="0" />
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput v-model="itemForm.remark" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="itemForm.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="itemDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleItemSubmit(itemFormRef)">提交</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 { commonService } from '@/api/commonApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Dictionary' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
// 左侧 类型
const typeLoading = ref(false)
const typeKeyword = ref<string>('')
const category_id = ref<string>('')
const typeTableData = ref<any[]>([])
const typePagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const activeType = ref<any | null>(null)
const typeFormRef = ref<FormInstance>()
const typeDialogVisible = ref(false)
const typeDialogType = ref<'add' | 'edit'>('add')
const typeForm = reactive<any>({ id: '', code: '', name: '', status: 1 })
const typeRules = reactive<FormRules>({
dict_code: [{ required: true, message: '请输入字典编码', trigger: 'blur' }],
dict_name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
category_id: [{ required: true, message: '请选择字典分类', trigger: 'blur' }]
})
const DictTypeSelect = ref<any[]>([])
const getDictTypeSelect = async () => {
const records = await commonService.getDictTypeSelect()
DictTypeSelect.value = records.sysDictItem
}
const getTypeList = async () => {
typeLoading.value = true
try {
const { currentPage, pageSize } = typePagination
const { records, current, size, total } = await systemService.getDictTypeList({
current: currentPage,
size: pageSize,
keyword: typeKeyword.value,
category_id: category_id.value
})
typeTableData.value = records
Object.assign(typePagination, { currentPage: current, pageSize: size, total })
if (!activeType.value && records && records.length) {
activeType.value = records[0]
await getItemList()
}
} finally {
typeLoading.value = false
}
}
const handleTypeRowClick = async (row: any) => {
activeType.value = row
itemPagination.currentPage = 1
await getItemList()
}
const handleTypeSizeChange = (val: number) => {
typePagination.pageSize = val
getTypeList()
}
const handleTypeCurrentChange = (val: number) => {
typePagination.currentPage = val
getTypeList()
}
const showTypeDialog = (type: 'add' | 'edit', row?: any) => {
typeDialogVisible.value = true
typeDialogType.value = type
if (type === 'edit' && row) {
Object.assign(typeForm, {
id: row.id,
dict_code: row.dict_code,
dict_name: row.dict_name,
description: row.description,
category_id: row.category_id,
status: row.status ?? 1
})
} else {
Object.assign(typeForm, {
id: '',
dict_code: '',
dict_name: '',
description: '',
status: 1,
category_id: ''
})
}
}
const handleTypeDelete = (row: any) => {
ElMessageBox.confirm('确定删除该字典类型吗?删除后其字典项也可能被清理。', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await systemService.dictTypeDelete({ id: row.id })
ElMessage.success(message)
if (activeType.value && activeType.value.id === row.id) {
activeType.value = null
itemTableData.value = []
itemPagination.total = 0
}
getTypeList()
})
}
const handleTypeSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
if (typeDialogType.value === 'add') {
const { message } = await systemService.dictTypeAdd({
dict_code: typeForm.dict_code,
dict_name: typeForm.dict_name,
description: typeForm.description,
status: typeForm.status,
category_id: typeForm.category_id
})
ElMessage.success(message)
} else {
const { message } = await systemService.dictTypeEdit({
id: typeForm.id,
dict_code: typeForm.dict_code,
dict_name: typeForm.dict_name,
description: typeForm.description,
category_id: typeForm.category_id,
status: typeForm.status
})
ElMessage.success(message)
}
typeDialogVisible.value = false
getTypeList()
})
}
// 右侧 字典项
const itemLoading = ref(false)
const itemKeyword = ref<string>('')
const itemTableData = ref<any[]>([])
const itemTableRef = ref()
const itemPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const itemFormRef = ref<FormInstance>()
const itemDialogVisible = ref(false)
const itemDialogType = ref<'add' | 'edit'>('add')
const itemForm = reactive<any>({
id: '',
item_label: '',
item_value: '',
sort: 0,
status: 1,
remark: ''
})
const itemRules = reactive<FormRules>({
item_label: [{ required: true, message: '请输入字典项', trigger: 'blur' }],
item_value: [{ required: true, message: '请输入描述', trigger: 'blur' }]
})
const getItemList = async () => {
if (!activeType.value) return
itemLoading.value = true
try {
const { currentPage, pageSize } = itemPagination
const { records, current, size, total } = await systemService.getDictItemList({
current: currentPage,
size: pageSize,
keyword: itemKeyword.value,
id: activeType.value.id
})
itemTableData.value = records
Object.assign(itemPagination, { currentPage: current, pageSize: size, total })
} finally {
itemLoading.value = false
}
}
const handleItemSizeChange = (val: number) => {
itemPagination.pageSize = val
getItemList()
}
const handleItemCurrentChange = (val: number) => {
itemPagination.currentPage = val
getItemList()
}
const showItemDialog = (type: 'add' | 'edit', row?: any) => {
itemDialogVisible.value = true
itemDialogType.value = type
if (type === 'edit' && row) {
Object.assign(itemForm, {
id: row.id,
item_label: row.item_label,
item_value: row.item_value,
sort: row.sort ?? 0,
status: row.status ?? 1,
remark: row.remark || ''
})
} else {
Object.assign(itemForm, {
id: '',
item_label: '',
item_value: '',
sort: 0,
status: 1,
remark: ''
})
}
}
const handleItemDelete = (row: any) => {
ElMessageBox.confirm('确定删除该字典项吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await systemService.dictItemDelete({ id: row.id })
ElMessage.success(message)
getItemList()
})
}
const handleItemSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl || !activeType.value) return
await formEl.validate(async (valid) => {
if (!valid) return
if (itemDialogType.value === 'add') {
const { message } = await systemService.dictItemAdd({
dict_id: activeType.value.id,
item_label: itemForm.item_label,
item_value: itemForm.item_value,
sort: itemForm.sort,
status: itemForm.status,
remark: itemForm.remark
})
ElMessage.success(message)
} else {
const { message } = await systemService.dictItemEdit({
id: itemForm.id,
dict_id: activeType.value.id,
item_label: itemForm.item_label,
item_value: itemForm.item_value,
sort: itemForm.sort,
status: itemForm.status,
remark: itemForm.remark
})
ElMessage.success(message)
}
itemDialogVisible.value = false
getItemList()
})
}
onMounted(async () => {
await getDictTypeSelect()
await getTypeList()
})
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.dict-layout {
.dict-type-card,
.dict-item-card {
background: var(--el-bg-color);
padding: 12px;
border-radius: 8px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.left {
font-weight: 500;
}
.right {
display: flex;
align-items: center;
}
.muted {
color: var(--el-text-color-secondary);
}
}
.type-pagination {
margin-top: 8px;
text-align: center;
}
}
</style>
+12 -2
View File
@@ -21,6 +21,8 @@
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="ID" prop="id" width="80" />
@@ -146,7 +148,7 @@
import { UserService } from '@/api/usersApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' })
defineOptions({ name: 'FeishuApp' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
@@ -209,7 +211,6 @@
getcompanyList()
getsystemsList()
})
// 获取列表数据
const getWorkList = async () => {
loading.value = true
@@ -230,6 +231,15 @@
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
const dialogType = ref('add')
+30 -8
View File
@@ -27,7 +27,7 @@
</template> -->
</el-segmented>
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
<ElButton
<!-- <ElButton
v-auth="'system@menu_add'"
@click="showModel('menu', null)"
v-ripple
@@ -37,12 +37,17 @@
</ElButton>
<ElButton @click="toggleExpand" v-ripple>
{{ isExpanded ? '收起' : '展开' }}
</ElButton>
</ElButton> -->
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton> -->
</template>
<template #right>
<div class="btnadds" @click="showModel('menu', null)" v-auth="'system@menu_add'">
<i class="iconfont-sys">&#xe83e;</i>
</div>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
@@ -178,22 +183,26 @@
<ElFormItem label="页面缓存" prop="keep_alive">
<ElSwitch
v-model="form.keep_alive"
:active-value="1"
:inactive-value="0"
:active-value="true"
:inactive-value="false"
></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否隐藏" prop="is_hide">
<ElSwitch v-model="form.is_hide" :active-value="1" :inactive-value="0"></ElSwitch>
<ElSwitch
v-model="form.is_hide"
:active-value="true"
:inactive-value="false"
></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="标签页是否隐藏" prop="is_hide_tab" label-width="120px">
<ElSwitch
v-model="form.is_hide_tab"
:active-value="1"
:inactive-value="0"
:active-value="true"
:inactive-value="false"
></ElSwitch>
</ElFormItem>
</ElCol>
@@ -491,7 +500,6 @@
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
system_id: [{ required: true, message: '请选择系统', trigger: 'blur' }],
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
})
@@ -748,4 +756,18 @@
}
}
}
.btnadds {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin-left: 10px;
color: var(--art-gray-700);
cursor: pointer;
background-color: rgba(var(--art-gray-200-rgb), 0.8);
border-radius: 6px;
transition: color 0.3s;
transition: all 0.3s;
}
</style>
+309
View File
@@ -0,0 +1,309 @@
<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">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@notice_add')"
>新增</ElButton
>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="编号" prop="msg_no" width="240px" />
<ElTableColumn label="通知标题" prop="title" />
<el-table-column prop="id" label="通知内容">
<template #default="scope">
<span v-html="scope.row.content" style="white-space: pre-line"></span>
</template>
</el-table-column>
<ElTableColumn label="推送类型" prop="push_method_name" />
<ElTableColumn label="场景" prop="scene_name" />
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button link @click="showfn(scope.row)"> 详情 </el-button>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('system@notice_destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog v-model="dialogVisible" title="新增通知" align-center>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
<ElFormItem label="通知标题" prop="title">
<ElInput v-model="form.title" />
</ElFormItem>
<ElFormItem label="通知内容" prop="content">
<ElInput v-model="form.content" type="textarea" :rows="3" placeholder="请输入通知内容" />
</ElFormItem>
<ElFormItem label="通知用户" prop="recipient">
<el-select
v-model="form.recipient"
multiple
filterable
placeholder="请选择要通知的用户"
style="width: 100%"
>
<el-option
v-for="item in companyUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-text class="mx-1" type="warning">不选择用户,代表通知所有用户!</el-text>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<ElDialog v-model="dialogVisiblexq" title="通知详情" align-center width="640px">
<div class="detail-dialog">
<div class="section">
<div class="info-grid">
<div class="label">发送人</div>
<div class="value">{{ noticeDetail.sender_name || '-' }}</div>
</div>
<div class="info-grid">
<div class="label">通知标题</div>
<div class="value">{{ noticeDetail.title || '-' }}</div>
</div>
</div>
<div class="section">
<div class="label" style="margin-bottom: 8px">通知内容</div>
<div class="content-box">
<div class="message-text" v-html="noticeDetail.content"></div>
</div>
</div>
</div>
</ElDialog>
</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 { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminNotice' })
const loading = ref(false)
const dialogVisible = ref(false)
const dialogVisiblexq = ref(false)
const formRef = ref<FormInstance>()
const { width } = useWindowSize()
const keyword = ref<string>('')
const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
abbr: [{ required: true, message: '请输入公司简称', trigger: 'blur' }]
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
})
const companyUserList = ref<any>()
// 获取用户列表
const getCompanyUserList = async () => {
const response = await manageService.presetusers()
companyUserList.value = response || []
}
const form = reactive<any>({
title: '',
content: '',
recipient: []
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
watch([keyword], () => {
pagination.currentPage = 1
getWorkList()
})
const noticeDetail = ref<any>({})
const showfn = async (row: any) => {
dialogVisiblexq.value = true
await systemService.getNoticeDetail({ id: row.id }).then((res: any) => {
noticeDetail.value = res
console.log(res)
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await systemService.getNoticeList({
current: currentPage,
size: pageSize
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const dialogType = ref('add')
const showDialog = (type: string) => {
getCompanyUserList()
dialogVisible.value = true
dialogType.value = type
form.title = ''
form.content = ''
form.recipient = []
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await systemService.delNotice({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
const { message } = await systemService.sendNotice(form)
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.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;
}
.message-text {
padding: 10px 14px;
line-height: 1.7;
white-space: pre-line;
word-break: break-word;
}
.detail-dialog {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 12px 14px;
}
.info-grid {
display: grid;
grid-template-columns: 96px 1fr;
gap: 6px 12px;
align-items: center;
padding: 6px 0;
}
.label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.value {
color: var(--el-text-color-primary);
font-size: 14px;
}
.content-box {
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
max-height: 320px;
overflow: auto;
}
</style>
+190 -75
View File
@@ -7,7 +7,9 @@
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple>搜索</ElButton>
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@role_add')">新增角色</ElButton>
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@role_add')"
>新增角色</ElButton
>
</ElCol>
</ElRow>
@@ -36,11 +38,12 @@
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button
<el-button link @click="showPermissionDialog(scope.row)">
<!-- <el-button
link
@click="showPermissionDialog(scope.row)"
v-if="hasAuth('system@role_perm')"
>
> -->
菜单权限
</el-button>
<el-button
@@ -92,39 +95,38 @@
</template>
</ElDialog>
<ElDialog
v-model="permissionDialog"
title="菜单权限"
width="520px"
align-center
class="el-dialog-border"
>
<ElScrollbar height="70vh">
<ElTree
ref="treeRef"
:data="processedMenuList"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="handleTreeCheck"
<ElDialog v-model="permissionDialog" title="菜单权限" align-center>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane
v-for="sys in systemsList"
:key="sys.id"
:label="sys.name"
:name="String(sys.id)"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</ElScrollbar>
<ElScrollbar height="70vh">
<ElTree
:ref="(el:any) => setTreeRef(sys.id, el)"
:data="processedMenuBySystem[sys.id] || []"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="() => handleTreeCheck(sys.id)"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</ElScrollbar>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
isSelectAll ? '取消全选' : '全部选择'
}}</ElButton>
<ElButton type="primary" @click="savePermission">保存</ElButton>
</div>
</template>
@@ -138,17 +140,29 @@
import { formatMenuTitle } from '@/router/utils/utils'
import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
import { UserService } from '@/api/usersApi'
const { hasAuth } = useAuth()
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
// import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
const loading = ref(false)
defineOptions({ name: 'Role' })
const activeName = ref('1')
const dialogVisible = ref(false)
const permissionDialog = ref(false)
// 权限树数据(来自接口 getPermissionMenuList)
const permissionMenuList = ref<any[]>([])
const permissionMenuListwork = ref<any[]>([])
const permissionMenuListcrm = ref<any[]>([])
// 精简后的多系统映射
const permissionMenuBySystem = reactive<Record<number, any[]>>({})
const processedMenuBySystem = reactive<Record<number, any[]>>({})
const treeRefs = reactive<Record<number, any>>({})
const setTreeRef = (systemId: number, el: any) => {
if (el) treeRefs[systemId] = el
}
const treeRef = ref()
const treeRefwork = ref()
const treeRefcrm = ref()
const isExpandAll = ref(true)
const isSelectAll = ref(false)
const pagination = reactive({
@@ -162,7 +176,12 @@
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
})
//系统
const systemsList = ref<any>()
const getsystemsList = async () => {
const response = await UserService.getsystemsList()
systemsList.value = response || []
}
const form = reactive<any>({
id: '',
name: '',
@@ -234,25 +253,46 @@
permissionDialog.value = true
roleId.value = row.id
try {
const res = await systemService.getPermissionMenuList({ role_id: row.id })
// 兼容不同返回结构(data/records/直接数组)
const list = (res as any)?.data ?? (res as any)?.records ?? (res as any)
permissionMenuList.value = Array.isArray(list) ? list : []
// 根据接口返回的 selected 字段设置默认选中
await getsystemsList()
// 并行请求所选用户的各系统权限
const sysIds = (systemsList.value || []).map((s: any) => s.id)
const results = await Promise.all(
sysIds.map((id: number) =>
systemService.getPermissionMenuList({ role_id: row.id, system_id: id })
)
)
sysIds.forEach((id: number, idx: number) => {
const raw = results[idx] as any
const list = raw?.data ?? raw?.records ?? raw
permissionMenuBySystem[id] = Array.isArray(list) ? list : []
processedMenuBySystem[id] = buildProcessedMenu(permissionMenuBySystem[id])
})
// 兼容旧变量,避免其它逻辑引用报错
permissionMenuList.value = permissionMenuBySystem[1] || []
permissionMenuListwork.value = permissionMenuBySystem[2] || []
permissionMenuListcrm.value = permissionMenuBySystem[3] || []
// 设置默认选中
nextTick(() => {
const keys = buildDefaultCheckedKeys(permissionMenuList.value)
treeRef.value?.setCheckedKeys(keys)
handleTreeCheck()
sysIds.forEach((id: number) => {
const keys = buildDefaultCheckedKeys(permissionMenuBySystem[id] || [])
treeRefs[id]?.setCheckedKeys?.(keys)
handleTreeCheck(id)
})
})
} catch (e) {
permissionMenuList.value = []
permissionMenuListwork.value = []
permissionMenuListcrm.value = []
}
}
// 构建默认选中的 key 列表(与 node-key="id" 一致)
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
const keys: Array<string | number> = []
const walk = (node: any) => {
if (node?.meta?.selected) {
const hasChildren = Array.isArray(node?.children) && node.children.length > 0
const hasAuths = Array.isArray(node?.meta?.authList) && node.meta.authList.length > 0
// 仅当没有子节点且没有权限子项时,才勾选父节点,避免联动导致子级被全选
if (node?.meta?.selected && !hasChildren && !hasAuths) {
keys.push(node.id)
}
if (Array.isArray(node?.meta?.authList)) {
@@ -262,7 +302,7 @@
}
})
}
if (Array.isArray(node?.children)) node.children.forEach(walk)
if (hasChildren) node.children.forEach(walk)
}
source.forEach(walk)
return keys
@@ -272,7 +312,6 @@
const processNode = (node: any) => {
// 规范化 id,确保树节点唯一键与 node-key 对应
const processed: any = { ...node, id: node.id }
// 如果有 authList,将其转换为子节点
if (node.meta && node.meta.authList && node.meta.authList.length) {
const authNodes = node.meta.authList.map((auth: any) => ({
@@ -285,27 +324,97 @@
isAuth: true,
checked: auth.selected || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
// 递归处理子节点
if (processed.children) {
processed.children = processed.children.map(processNode)
}
return processed
}
const source = permissionMenuList.value || []
return source.map(processNode)
})
const processedMenuListwork = computed(() => {
const processNode = (node: any) => {
// 规范化 id,确保树节点唯一键与 node-key 对应
const processed: any = { ...node, id: node.id }
// 如果有 authList,将其转换为子节点
if (node.meta && node.meta.authList && node.meta.authList.length) {
const authNodes = node.meta.authList.map((auth: any) => ({
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
id: `${node.id}__auth__${auth.authMark}`,
name: `${node.name}_${auth.authMark}`,
label: auth.title,
auth_mark: auth.authMark,
auth_id: auth.id,
isAuth: true,
checked: auth.selected || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
// 递归处理子节点
if (processed.children) {
processed.children = processed.children.map(processNode)
}
return processed
}
const source = permissionMenuListwork.value || []
return source.map(processNode)
})
const processedMenuListcrm = computed(() => {
const processNode = (node: any) => {
// 规范化 id,确保树节点唯一键与 node-key 对应
const processed: any = { ...node, id: node.id }
// 如果有 authList,将其转换为子节点
if (node.meta && node.meta.authList && node.meta.authList.length) {
const authNodes = node.meta.authList.map((auth: any) => ({
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
id: `${node.id}__auth__${auth.authMark}`,
name: `${node.name}_${auth.authMark}`,
label: auth.title,
auth_mark: auth.authMark,
auth_id: auth.id,
isAuth: true,
checked: auth.selected || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
// 递归处理子节点
if (processed.children) {
processed.children = processed.children.map(processNode)
}
return processed
}
const source = permissionMenuListcrm.value || []
return source.map(processNode)
})
const defaultProps = {
children: 'children',
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
}
// 通用:处理节点,展开权限为子节点
const processNode = (node: any) => {
const processed: any = { ...node, id: node.id }
if (node.meta && node.meta.authList && node.meta.authList.length) {
const authNodes = node.meta.authList.map((auth: any) => ({
id: `${node.id}__auth__${auth.authMark}`,
name: `${node.name}_${auth.authMark}`,
label: auth.title,
auth_mark: auth.authMark,
auth_id: auth.id,
isAuth: true,
checked: auth.selected || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
if (processed.children) processed.children = processed.children.map(processNode)
return processed
}
const buildProcessedMenu = (source: any[]) => (source || []).map(processNode)
const deleteRole = async (row: any) => {
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
confirmButtonText: '确定',
@@ -335,46 +444,33 @@
dialogVisible.value = false
formEl.resetFields()
}
const getSelectedIds = () => {
const tree = treeRef.value
if (!tree) return { menuIds: [], authIds: [] }
const getSelectedIdsBySystem = (systemId: number) => {
const tree = treeRefs[systemId]
if (!tree) return [] as (string | number)[]
const checkedNodes = tree.getCheckedNodes?.(false) || []
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
const allNodes = [...checkedNodes, ...halfCheckedNodes]
const menuIds = Array.from(
new Set(
allNodes
.filter((n: any) => !n.isAuth && n.id !== undefined && n.id !== null)
.map((n: any) => n.id)
)
new Set(allNodes.filter((n: any) => !n.isAuth && n.id != null).map((n: any) => n.id))
)
const authIds = Array.from(
new Set(
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
)
)
const ids = Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
return { menuIds, authIds, ids }
return Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
}
const savePermission = async () => {
const { ids } = getSelectedIds()
// console.log('选中的菜单ID:', menuIds)
// console.log('选中的权限ID:', authIds)
// console.log('合并后的全部ID:', ids)
const sysIds = (systemsList.value || []).map((s: any) => s.id)
const idall = Array.from(new Set(sysIds.flatMap((id: number) => getSelectedIdsBySystem(id))))
console.log('idall:', idall)
const res = await systemService.roleAuth({
role_id: roleId.value,
resources: ids
resources: idall
})
ElMessage.success(res?.message || '权限保存成功')
permissionDialog.value = false
}
const toggleExpandAll = () => {
const tree = treeRef.value
if (!tree) return
@@ -420,13 +516,32 @@
return keys
}
const handleTreeCheck = () => {
const tree = treeRef.value
const handleTreeCheck = (systemId?: number) => {
const sysId = systemId ?? Number(activeName.value)
const tree = treeRefs[sysId] || treeRef.value
if (!tree) return
const checkedKeys = tree.getCheckedKeys?.() || []
const allKeys = getAllNodeKeys(processedMenuBySystem[sysId] || [])
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
}
const handleTreeCheckwork = () => {
const tree = treeRefwork.value
if (!tree) return
// 使用树组件的getCheckedKeys方法获取选中的节点
const checkedKeys = tree.getCheckedKeys()
const allKeys = getAllNodeKeys(processedMenuList.value)
const allKeys = getAllNodeKeys(processedMenuListwork.value)
// 判断是否全选:选中的节点数量等于总节点数量
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
}
const handleTreeCheckcrm = () => {
const tree = treeRefcrm.value
if (!tree) return
// 使用树组件的getCheckedKeys方法获取选中的节点
const checkedKeys = tree.getCheckedKeys()
const allKeys = getAllNodeKeys(processedMenuListcrm.value)
// 判断是否全选:选中的节点数量等于总节点数量
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
+12 -2
View File
@@ -20,6 +20,8 @@
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="ID" prop="id" width="80" />
@@ -153,7 +155,7 @@
import { systemService } from '@/api/systemApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' })
defineOptions({ name: 'Systems' })
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
@@ -236,7 +238,15 @@
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 系统类型相关辅助函数
const getTypeText = (type: string) => {
const typeMap: { [key: string]: string } = {
+254 -74
View File
@@ -6,6 +6,17 @@
<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="company_id" style="min-width: 120px" placeholder="请选择" clearable>
<el-option
v-for="item in companyList"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态:" label-width="50px">
<el-select v-model="selectValue" style="min-width: 120px">
<el-option label="全部" value="" />
@@ -14,6 +25,9 @@
</el-select>
</el-form-item>
</div>
<el-button type="primary" @click="leaveUserfn">
<i class="iconfont-sys">&#xe794;</i> 离职客户分配
</el-button>
</div>
<div class="boxshadow">
<!-- 表格 -->
@@ -27,7 +41,7 @@
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
lass="customer-table"
class="customer-table"
>
<template #default>
<!-- <el-table-column prop="id" label="用户ID" align="center" /> -->
@@ -117,7 +131,7 @@
@selection-change="handleSelectionChange"
@size-change="handleSizeChangekh"
@current-change="handleCurrentChangekh"
lass="customer-table"
class="customer-table"
>
<template #default>
<el-table-column type="selection" width="55px" align="center"></el-table-column>
@@ -125,9 +139,20 @@
<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="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="follow" label="联系方式" width="240px" show-overflow-tooltip>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<el-table-column prop="grade_txt" label="等级" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
@@ -136,6 +161,13 @@
</span>
</template>
</el-table-column>
<el-table-column
prop="code"
label="编号"
align="center"
min-width="120px"
show-overflow-tooltip
/>
</template>
</ArtTable>
</div>
@@ -227,39 +259,38 @@
:hide-on-click-modal="true"
@close="showImageViewer = false"
/>
<ElDialog
v-model="permissionDialog"
title="菜单权限"
width="520px"
align-center
class="el-dialog-border"
>
<ElScrollbar height="70vh">
<ElTree
ref="treeRef"
:data="processedMenuList"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="handleTreeCheck"
<ElDialog v-model="permissionDialog" title="菜单权限" align-center>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane
v-for="sys in systemsList"
:key="sys.id"
:label="sys.name"
:name="String(sys.id)"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</ElScrollbar>
<ElScrollbar height="70vh">
<ElTree
:ref="(el: any) => setTreeRef(sys.id, el)"
:data="processedMenuBySystem[sys.id] || []"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="() => handleTreeCheck(sys.id)"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</ElScrollbar>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
isSelectAll ? '取消全选' : '全部选择'
}}</ElButton>
<ElButton type="primary" @click="savePermission">保存</ElButton>
</div>
</template>
@@ -318,18 +349,72 @@
</div>
</el-form>
</el-dialog>
<!-- 离职客户分配对话框 -->
<el-dialog
title="离职客户分配"
v-model="leaveUserDialogVisible"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="leaveUserForm" ref="leaveUserFormRef" label-width="80px" class="add-form">
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="公司" prop="corp_id">
<el-select
v-model="leaveUserForm.corp_id"
filterable
placeholder="请选择公司"
style="width: 100%"
>
<el-option
v-for="item in companyList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="用户" prop="user_id">
<el-select
v-model="leaveUserForm.user_id"
multiple
filterable
placeholder="请选择要分配的用户"
style="width: 100%"
>
<el-option
v-for="item in companyUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<div class="form-submit-row">
<el-button @click="leaveUserDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleLeaveUserSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { ElImageViewer } from 'element-plus'
import { ref, reactive, onMounted, watch, computed, nextTick } from 'vue'
import { ElImageViewer, ElMessage } from 'element-plus'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { systemService } from '@/api/systemApi'
import { formatMenuTitle } from '@/router/utils/utils'
import { UserService } from '@/api/usersApi'
defineOptions({ name: 'UserList' })
const { hasAuth } = useAuth()
const selectValue = ref<string[]>([])
const activeName = ref('1')
const selectValue = ref('1')
const company_id = ref('')
const keyword = ref<string>('')
const loading = ref(false)
const loadingkh = ref(false)
@@ -338,8 +423,18 @@
const AddDialogVisible = ref(false)
const permissionDialog = ref(false)
const roleDialogVisible = ref(false)
const leaveUserDialogVisible = ref(false)
// 权限树数据(来自接口 getPermissionMenuList)
const permissionMenuList = ref<any[]>([])
const permissionMenuListwork = ref<any[]>([])
const permissionMenuListcrm = ref<any[]>([])
// 精简后的多系统映射
const permissionMenuBySystem = reactive<Record<number, any[]>>({})
const processedMenuBySystem = reactive<Record<number, any[]>>({})
const treeRefs = reactive<Record<number, any>>({})
const setTreeRef = (systemId: number, el: any) => {
if (el) treeRefs[systemId] = el
}
const treeRef = ref()
const isExpandAll = ref(true)
const isSelectAll = ref(false)
@@ -373,13 +468,29 @@
role_ids: '',
system_ids: ''
})
const leaveUserForm = reactive({
corp_id: '',
user_id: []
})
const userOptions = ref<any>()
//系统
const systemsList = ref<any>()
const companyList = ref<any>()
const companyUserList = ref<any>()
const getsystemsList = async () => {
const response = await UserService.getsystemsList()
systemsList.value = response || []
}
// 获取公司列表
const getCompanyList = async () => {
const response = await UserService.getCompanyList()
companyList.value = response || []
}
// 获取用户列表
const getCompanyUserList = async () => {
const response = await manageService.presetusers()
companyUserList.value = response || []
}
//角色下拉框
const roleOptions = ref<any>()
const getroleOptions = async () => {
@@ -396,11 +507,12 @@
const tableData = ref<any[]>([])
const tableDatakh = ref<any[]>([])
onMounted(() => {
getCompanyList()
getuserList()
gettableList()
})
// tabs 切换联动
watch([selectValue, keyword], () => {
watch([selectValue, keyword, company_id], () => {
pagination.currentPage = 1
gettableList()
})
@@ -415,6 +527,7 @@
if (selectValue.value) {
params.status = selectValue.value
}
params.corp_id = company_id.value
if (keyword.value) {
params.keyword = keyword.value
}
@@ -502,25 +615,50 @@
permissionDialog.value = true
userId.value = row.id
try {
const res = await systemService.getPermissionMenuList({ user_id: row.id })
// 兼容不同返回结构(data/records/直接数组)
const list = (res as any)?.data ?? (res as any)?.records ?? (res as any)
permissionMenuList.value = Array.isArray(list) ? list : []
// 根据接口返回的 selected 字段设置默认选中
await getsystemsList()
// 并行请求所选用户的各系统权限
const sysIds = (systemsList.value || []).map((s: any) => s.id)
const results = await Promise.all(
sysIds.map((id: number) =>
systemService.getPermissionMenuList({ user_id: row.id, system_id: id })
)
)
sysIds.forEach((id: number, idx: number) => {
const raw = results[idx] as any
const list = raw?.data ?? raw?.records ?? raw
permissionMenuBySystem[id] = Array.isArray(list) ? list : []
processedMenuBySystem[id] = buildProcessedMenu(permissionMenuBySystem[id])
})
// 兼容旧变量,避免其它逻辑引用报错
permissionMenuList.value = permissionMenuBySystem[1] || []
permissionMenuListwork.value = permissionMenuBySystem[2] || []
permissionMenuListcrm.value = permissionMenuBySystem[3] || []
// 设置默认选中
nextTick(() => {
const keys = buildDefaultCheckedKeys(permissionMenuList.value)
treeRef.value?.setCheckedKeys(keys)
handleTreeCheck()
sysIds.forEach((id: number) => {
const keys = buildDefaultCheckedKeys(permissionMenuBySystem[id] || [])
treeRefs[id]?.setCheckedKeys?.(keys)
handleTreeCheck(id)
})
})
} catch (e) {
;(systemsList.value || []).forEach((s: any) => {
permissionMenuBySystem[s.id] = []
processedMenuBySystem[s.id] = []
})
permissionMenuList.value = []
permissionMenuListwork.value = []
permissionMenuListcrm.value = []
}
}
// 构建默认选中的 key 列表(与 node-key="id" 一致)
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
const keys: Array<string | number> = []
const walk = (node: any) => {
if (node?.meta?.selected) {
const hasChildren = Array.isArray(node?.children) && node.children.length > 0
const hasAuths = Array.isArray(node?.meta?.authList) && node.meta.authList.length > 0
// 仅当没有子节点且没有权限子项时,才勾选父节点,避免级联误选
if (node?.meta?.selected && !hasChildren && !hasAuths) {
keys.push(node.id)
}
if (Array.isArray(node?.meta?.authList)) {
@@ -530,7 +668,7 @@
}
})
}
if (Array.isArray(node?.children)) node.children.forEach(walk)
if (hasChildren) node.children.forEach(walk)
}
source.forEach(walk)
return keys
@@ -568,11 +706,29 @@
const source = permissionMenuList.value || []
return source.map(processNode)
})
const defaultProps = {
children: 'children',
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
}
// 通用:处理节点,展开权限为子节点
const processNode = (node: any) => {
const processed: any = { ...node, id: node.id }
if (node.meta && node.meta.authList && node.meta.authList.length) {
const authNodes = node.meta.authList.map((auth: any) => ({
id: `${node.id}__auth__${auth.authMark}`,
name: `${node.name}_${auth.authMark}`,
label: auth.title,
auth_mark: auth.authMark,
auth_id: auth.id,
isAuth: true,
checked: auth.selected || false
}))
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
}
if (processed.children) processed.children = processed.children.map(processNode)
return processed
}
const buildProcessedMenu = (source: any[]) => (source || []).map(processNode)
const toggleExpandAll = () => {
const tree = treeRef.value
if (!tree) return
@@ -616,47 +772,36 @@
traverse(nodes)
return keys
}
const handleTreeCheck = () => {
const tree = treeRef.value
const handleTreeCheck = (systemId?: number) => {
const sysId = systemId ?? Number(activeName.value)
const tree = treeRefs[sysId] || treeRef.value
if (!tree) return
// 使用树组件的getCheckedKeys方法获取选中的节点
const checkedKeys = tree.getCheckedKeys()
const allKeys = getAllNodeKeys(processedMenuList.value)
// 判断是否全选:选中的节点数量等于总节点数量
const checkedKeys = tree.getCheckedKeys?.() || []
const allKeys = getAllNodeKeys(processedMenuBySystem[sysId] || [])
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
}
const getSelectedIds = () => {
const tree = treeRef.value
if (!tree) return { menuIds: [], authIds: [] }
const getSelectedIdsBySystem = (systemId: number) => {
const tree = treeRefs[systemId]
if (!tree) return [] as (string | number)[]
const checkedNodes = tree.getCheckedNodes?.(false) || []
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
const allNodes = [...checkedNodes, ...halfCheckedNodes]
const menuIds = Array.from(
new Set(
allNodes
.filter((n: any) => !n.isAuth && n.id !== undefined && n.id !== null)
.map((n: any) => n.id)
)
new Set(allNodes.filter((n: any) => !n.isAuth && n.id != null).map((n: any) => n.id))
)
const authIds = Array.from(
new Set(
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
)
)
const ids = Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
return { menuIds, authIds, ids }
return Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
}
const savePermission = async () => {
const { ids } = getSelectedIds()
const sysIds = (systemsList.value || []).map((s: any) => s.id)
const idall = Array.from(new Set(sysIds.flatMap((id: number) => getSelectedIdsBySystem(id))))
const res = await manageService.userMenuAuth({
user_id: userId.value,
resources: ids
resources: idall
})
ElMessage.success(res?.message || '权限保存成功')
permissionDialog.value = false
@@ -714,6 +859,41 @@
showImageViewer.value = true
}
}
//离职客户分配
const leaveUserfn = () => {
getCompanyUserList()
leaveUserDialogVisible.value = true
leaveUserForm.corp_id = ''
leaveUserForm.user_id = []
companyUserList.value = []
}
// 提交离职客户分配
const handleLeaveUserSubmit = async () => {
if (!leaveUserForm.corp_id) {
ElMessage.warning('请选择公司')
return
}
if (!leaveUserForm.user_id || leaveUserForm.user_id.length === 0) {
ElMessage.warning('请选择用户')
return
}
try {
// 将 user_id 数组转换为逗号分隔的字符串
const submitData = {
...leaveUserForm,
user_id: leaveUserForm.user_id.join(',')
}
// 这里调用离职客户分配的API
const res = await UserService.leaveUser(submitData)
ElMessage.success(res?.message || '离职客户分配成功')
leaveUserDialogVisible.value = false
gettableList()
} catch (error) {
console.error('离职客户分配失败:', error)
}
}
</script>
<style scoped lang="scss">
+91 -12
View File
@@ -116,6 +116,50 @@
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="form.email" :disabled="!isEdit" />
</ElFormItem>
<ElFormItem label="签名" prop="signature" class="right-input">
<template v-if="form.sign_img">
<img :src="sign_img_url" class="signature-img" />
<el-button
v-if="isEdit"
style="margin-left: 12px"
@click="showReSignQR = !showReSignQR"
>
重新签名
</el-button>
<div
v-if="isEdit && showReSignQR"
class="signature-qrcode"
style="margin-top: 20px; margin-left: 20px"
>
<qrcode-vue
:value="qrcodeUrl"
:size="200"
level="M"
render-as="svg"
:include-margin="true"
background="#ffffff"
foreground="#000000"
/>
<div class="qrcode-tip">手机扫码重新签名</div>
</div>
</template>
<template v-else>
<div class="signature-qrcode">
<qrcode-vue
:value="qrcodeUrl"
:size="200"
level="M"
render-as="svg"
:include-margin="true"
background="#ffffff"
foreground="#000000"
/>
<div class="qrcode-tip">手机扫码进行签名</div>
</div>
</template>
<!-- <el-button @click="handleSignature" type="primary">签名</el-button> -->
<el-input type="hidden" v-model="form.signature" />
</ElFormItem>
</ElRow>
<!-- <ElRow>
<ElFormItem
@@ -218,15 +262,18 @@
import { UserService } from '@/api/usersApi'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { useUserStore } from '@/store/modules/user'
import { computed } from 'vue'
import QrcodeVue from 'qrcode.vue'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
defineOptions({ name: 'UserCenter' })
defineOptions({ name: 'AdminUserCenter' })
// 定义客户详情类型
interface CustomerInfo {
[key: string]: any
}
// const imageUrls = ref(imageUrl)
const imageUrlavatar = ref('')
const sign_img_url = ref('')
// const socialize_pic_full = ref('')
// const countryOptions = ref<any>()
const form = ref<CustomerInfo>({})
@@ -240,16 +287,23 @@
// countryOptions.value = res||[]
// } catch (error) { countryOptions.value = [] }
// }
const { getUserInfo } = useUserStore()
// 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
const data = await UserService.getUserInfo()
userInfo.value = data.user
form.value = data.user
imageUrlavatar.value = data.user.avatar
// socialize_pic_full.value = data.socialize_pic_full
sign_img_url.value = data.user.sign_img_url
// userInfo.value = getUserInfo.user
// form.value = getUserInfo.user
// imageUrlavatar.value = getUserInfo.user.avatar
// sign_img_url.value = getUserInfo.user.sign_img_url
// socialize_pic_full.value = data.socialize_pic_full
}
const isEdit = ref(false)
const showReSignQR = ref(false)
const isEditPwd = ref(false)
const pwdForm = reactive({
password: '',
@@ -265,11 +319,11 @@
label: '未知'
},
{
value: '1',
value: 1,
label: '男'
},
{
value: '2',
value: 2,
label: '女'
}
]
@@ -277,13 +331,35 @@
// await getarea()
await getuserInfo()
})
const qrcodeUrl = computed(() => {
const userStore = useUserStore() as any
const userId = form.value?.id || userInfo.value?.id || ''
const base = location.origin
const params = new URLSearchParams({ userId: String(userId) })
// 只保留必要的参数,减少URL长度
if (userStore?.accessToken) params.append('token', userStore.accessToken)
if (userStore?.companyabbr) params.append('corp', userStore.companyabbr)
if (userStore?.companyid) params.append('corpid', userStore.companyid)
if (userStore?.systemsabbr) params.append('sys', userStore.systemsabbr)
if (userStore?.systemsid) params.append('sysid', userStore.systemsid)
if (userStore?.loginType) params.append('type', userStore.loginType)
if (userStore?.language) params.append('lang', userStore.language)
return `${base}/auth/signature/mobile?${params.toString()}`
})
const edit = async () => {
if (isEdit.value) {
const res = await UserService.editUserInfo(form.value)
const payload: any = { ...(form.value as any) }
delete payload.sign_img_url
const res = await UserService.editUserInfo(payload)
ElMessage.success(res?.message || '操作成功')
getuserInfo()
}
isEdit.value = !isEdit.value
if (isEdit.value === false) {
showReSignQR.value = false
}
}
const editPwd = async () => {
@@ -328,7 +404,7 @@
</style>
<style lang="scss" scoped>
:deep(.qrcode-radio-group .el-form-item__content){
:deep(.qrcode-radio-group .el-form-item__content) {
height: 40px;
}
.avatar {
@@ -337,7 +413,7 @@
border-radius: 50%;
object-fit: cover;
}
.qrcode-img{
.qrcode-img {
width: 80px;
height: 80px;
}
@@ -372,7 +448,7 @@
overflow: hidden;
text-align: center;
// background: var(--art-main-bg-color);
background: linear-gradient(180deg, #E9F0FF 0%, #FFFFFF 67.9%);
background: linear-gradient(180deg, #e9f0ff 0%, #ffffff 67.9%);
border-radius: $box-radius;
.avatar {
@@ -530,4 +606,7 @@
}
}
}
.signature-img {
width: 160px;
}
</style>
+654
View File
@@ -0,0 +1,654 @@
<template>
<div class="page-content">
<div v-if="!isMobile" class="dict-layout">
<ElRow :gutter="16">
<ElCol :xs="24" :sm="10" :lg="10">
<div class="dict-type-card">
<div class="card-header">
<div style="display: flex; align-items: center">
<ElInput
v-model="brandKeyword"
placeholder="搜索品牌名称/编码"
clearable
@keyup.enter="getBrandList"
/>
<ElButton style="margin-left: 8px" @click="getBrandList" v-ripple>搜索</ElButton>
</div>
<ElButton
type="primary"
style="margin-left: 8px"
@click="showBrandDialog('add')"
v-if="hasAuth('brand@add')"
>新增品牌</ElButton
>
</div>
<el-table
:data="brandTableData"
v-loading="brandLoading"
height="calc(100vh - 280px)"
@row-click="handleBrandRowClick"
:highlight-current-row="true"
:row-key="'id'"
>
<el-table-column prop="brand_code" label="品牌编码" />
<el-table-column prop="brand_name" label="品牌名称" />
<el-table-column label="Logo" width="80">
<template #default="scope">
<div class="brand-logo-cell">
<img
v-if="scope.row.logo_full"
:src="scope.row.logo_full"
:alt="scope.row.brand_name"
/>
<div v-else class="logo-placeholder">{{ scope.row.brand_name?.charAt(0) }}</div>
</div>
</template>
</el-table-column>
<!-- <el-table-column prop="description" label="描述" show-overflow-tooltip /> -->
<!-- <el-table-column prop="sort" label="排序" width="80" /> -->
<el-table-column label="是否热门" prop="hot">
<template #default="scope">
<ElTag :type="scope.row.hot == 1 ? 'danger' : 'info'">
{{ scope.row.hot == 1 ? '是' : '否' }}
</ElTag>
</template>
</el-table-column>
<el-table-column label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">
{{ scope.row.status ? '启用' : '停用' }}
</ElTag>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" min-width="140" align="center">
<template #default="scope">
<el-button
link
size="small"
@click.stop="showBrandDialog('edit', scope.row)"
v-if="hasAuth('brand@update')"
>编辑</el-button
>
<el-button
link
type="danger"
size="small"
@click.stop="handleBrandDelete(scope.row)"
v-if="hasAuth('brand@destroy')"
>删除</el-button
>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无品牌" v-show="!brandLoading" />
</template>
</el-table>
<div class="type-pagination">
<el-pagination
background
layout="total, prev, pager, next"
:current-page="brandPagination.currentPage"
:page-size="brandPagination.pageSize"
:total="brandPagination.total"
:page-sizes="[10, 20, 30, 50]"
:pager-count="4"
@current-change="handleBrandCurrentChange"
@size-change="handleBrandSizeChange"
/>
</div>
</div>
</ElCol>
<ElCol :xs="24" :sm="14" :lg="14">
<div class="dict-item-card">
<div class="card-header">
<div class="left">
<span v-if="activeBrand"
>当前品牌:{{ activeBrand.brand_name }}({{ activeBrand.brand_code }})</span
>
<span v-else class="muted">请选择左侧车辆品牌</span>
<ElButton
type="primary"
style="margin-left: 8px"
@click="showSeriesDialog('add')"
:disabled="!activeBrand"
v-if="hasAuth('series@add')"
>新增系列</ElButton
>
</div>
<div class="right">
<ElInput
v-model="seriesKeyword"
placeholder="搜索系列名称/编码"
clearable
style="width: 240px"
@keyup.enter="getSeriesList"
/>
<ElButton style="margin-left: 8px" @click="getSeriesList" v-ripple>搜索</ElButton>
</div>
</div>
<ArtTable
ref="seriesTableRef"
row-key="id"
:loading="seriesLoading"
:data="seriesTableData"
:currentPage="seriesPagination.currentPage"
:pageSize="seriesPagination.pageSize"
:total="seriesPagination.total"
@size-change="handleSeriesSizeChange"
@current-change="handleSeriesCurrentChange"
>
<template #default>
<ElTableColumn label="系列编码" prop="series_code" min-width="120" />
<ElTableColumn label="系列名称" prop="series_name" min-width="140" />
<ElTableColumn
label="描述"
prop="description"
min-width="160"
show-overflow-tooltip
/>
<!-- <ElTableColumn label="排序" prop="sort" width="80" /> -->
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
scope.row.status ? '启用' : '停用'
}}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn fixed="right" label="操作" min-width="140px" align="center">
<template #default="scope">
<ElRow>
<el-button
link
@click="showSeriesDialog('edit', scope.row)"
v-if="hasAuth('series@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="handleSeriesDelete(scope.row)"
v-if="hasAuth('series@destroy')"
>删除</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
</ElCol>
</ElRow>
<ElDialog
v-model="brandDialogVisible"
:title="brandDialogType === 'add' ? '新增品牌' : '编辑品牌'"
width="750px"
align-center
>
<ElForm ref="brandFormRef" :model="brandForm" :rules="brandRules" label-width="100px">
<ElFormItem label="品牌编码" prop="brand_code">
<ElInput v-model="brandForm.brand_code" />
</ElFormItem>
<ElFormItem label="品牌名称" prop="brand_name">
<ElInput v-model="brandForm.brand_name" />
</ElFormItem>
<ElFormItem label="品牌Logo" prop="logo">
<ElUpload
class="logo-uploader"
:action="actionImageUplode"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleLogoSuccess"
:before-upload="beforeLogoUpload"
accept="image/*"
>
<img v-if="brandForm.logo" :src="logourl" class="logo-preview" />
<ElIcon v-else class="logo-uploader-icon"><Plus /></ElIcon>
</ElUpload>
</ElFormItem>
<ElFormItem label="是否热门" prop="hot">
<el-radio-group v-model="brandForm.hot">
<el-radio :label="1">是</el-radio>
<el-radio :label="0">否</el-radio>
</el-radio-group>
</ElFormItem>
<ElFormItem label="品牌描述" prop="description">
<ElInput v-model="brandForm.description" type="textarea" :rows="3" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="brandForm.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="brandForm.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="brandDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleBrandSubmit(brandFormRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<ElDialog
v-model="seriesDialogVisible"
:title="seriesDialogType === 'add' ? '新增系列' : '编辑系列'"
width="750px"
align-center
>
<ElForm ref="seriesFormRef" :model="seriesForm" :rules="seriesRules" label-width="100px">
<ElFormItem label="所属品牌">
<ElInput
:model-value="
activeBrand ? `${activeBrand.brand_name}(${activeBrand.brand_code})` : ''
"
disabled
/>
</ElFormItem>
<!-- <ElFormItem label="系列编码" prop="series_code">
<ElInput v-model="seriesForm.series_code" />
</ElFormItem> -->
<ElFormItem label="系列名称" prop="series_name">
<ElInput v-model="seriesForm.series_name" />
</ElFormItem>
<ElFormItem label="系列描述" prop="description">
<ElInput v-model="seriesForm.description" type="textarea" :rows="3" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="seriesForm.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="seriesForm.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="seriesDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSeriesSubmit(seriesFormRef)">提交</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 { Plus } from '@element-plus/icons-vue'
import { vehicleService } from '@/api/vehicleApi'
import { useAuth } from '@/composables/useAuth'
import { useWindowSize } from '@vueuse/core'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminVehicleBrand' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const logourl = ref('')
// 左侧 品牌
const brandLoading = ref(false)
const brandKeyword = ref<string>('')
const brandTableData = ref<any[]>([])
const brandPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const activeBrand = ref<any | null>(null)
const brandFormRef = ref<FormInstance>()
const brandDialogVisible = ref(false)
const brandDialogType = ref<'add' | 'edit'>('add')
const brandForm = reactive<any>({
id: '',
brand_code: '',
brand_name: '',
logo: '',
hot: 0,
description: '',
sort: 0,
status: 1
})
const brandRules = reactive<FormRules>({
brand_code: [{ required: true, message: '请输入品牌编码', trigger: 'blur' }],
brand_name: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }]
})
const getBrandList = async () => {
brandLoading.value = true
try {
const { currentPage, pageSize } = brandPagination
const { records, current, size, total } = await vehicleService.getBrandList({
current: currentPage,
size: pageSize,
keyword: brandKeyword.value
})
brandTableData.value = records
Object.assign(brandPagination, { currentPage: current, pageSize: size, total })
if (!activeBrand.value && records && records.length) {
activeBrand.value = records[0]
await getSeriesList()
}
} finally {
brandLoading.value = false
}
}
const handleBrandRowClick = async (row: any) => {
activeBrand.value = row
seriesPagination.currentPage = 1
await getSeriesList()
}
const handleBrandSizeChange = (val: number) => {
brandPagination.pageSize = val
getBrandList()
}
const handleBrandCurrentChange = (val: number) => {
brandPagination.currentPage = val
getBrandList()
}
const showBrandDialog = (type: 'add' | 'edit', row?: any) => {
brandDialogVisible.value = true
brandDialogType.value = type
if (type === 'edit' && row) {
logourl.value = row.logo_full
Object.assign(brandForm, {
id: row.id,
brand_code: row.brand_code,
brand_name: row.brand_name,
hot: row.hot,
logo: row.logo,
description: row.description,
sort: row.sort ?? 0,
status: row.status ?? 1
})
} else {
Object.assign(brandForm, {
id: '',
brand_code: '',
brand_name: '',
logo: '',
hot: 0,
description: '',
sort: 0,
status: 1
})
}
}
const handleBrandDelete = (row: any) => {
ElMessageBox.confirm('确定删除该品牌吗?删除后其系列也可能被清理。', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await vehicleService.brandDelete({ id: row.id })
ElMessage.success(message)
if (activeBrand.value && activeBrand.value.id === row.id) {
activeBrand.value = null
seriesTableData.value = []
seriesPagination.total = 0
}
getBrandList()
})
}
const handleBrandSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
if (brandDialogType.value === 'add') {
const { message } = await vehicleService.brandAdd({
brand_code: brandForm.brand_code,
brand_name: brandForm.brand_name,
logo: brandForm.logo,
hot: brandForm.hot,
description: brandForm.description,
sort: brandForm.sort,
status: brandForm.status
})
ElMessage.success(message)
} else {
const { message } = await vehicleService.brandEdit(brandForm)
ElMessage.success(message)
}
brandDialogVisible.value = false
getBrandList()
})
}
// 右侧 系列
const seriesLoading = ref(false)
const seriesKeyword = ref<string>('')
const seriesTableData = ref<any[]>([])
const seriesTableRef = ref()
const seriesPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const seriesFormRef = ref<FormInstance>()
const seriesDialogVisible = ref(false)
const seriesDialogType = ref<'add' | 'edit'>('add')
const seriesForm = reactive<any>({
series_code: '',
series_name: '',
description: '',
sort: 0,
status: 1
})
const seriesRules = reactive<FormRules>({
series_name: [{ required: true, message: '请输入系列名称', trigger: 'blur' }]
})
const getSeriesList = async () => {
if (!activeBrand.value) return
seriesLoading.value = true
try {
const { currentPage, pageSize } = seriesPagination
const { records, current, size, total } = await vehicleService.getSeriesList({
current: currentPage,
size: pageSize,
keyword: seriesKeyword.value,
brand_id: activeBrand.value.id
})
seriesTableData.value = records
Object.assign(seriesPagination, { currentPage: current, pageSize: size, total })
} finally {
seriesLoading.value = false
}
}
const handleSeriesSizeChange = (val: number) => {
seriesPagination.pageSize = val
getSeriesList()
}
const handleSeriesCurrentChange = (val: number) => {
seriesPagination.currentPage = val
getSeriesList()
}
const showSeriesDialog = (type: 'add' | 'edit', row?: any) => {
seriesDialogVisible.value = true
seriesDialogType.value = type
if (type === 'edit' && row) {
Object.assign(seriesForm, {
id: row.id,
series_code: row.series_code,
series_name: row.series_name,
description: row.description,
sort: row.sort ?? 0,
status: row.status ?? 1
})
} else {
Object.assign(seriesForm, {
id: '',
series_code: '',
series_name: '',
description: '',
sort: 0,
status: 1
})
}
}
const handleSeriesDelete = (row: any) => {
ElMessageBox.confirm('确定删除该系列吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await vehicleService.seriesDelete({ id: row.id })
ElMessage.success(message)
getSeriesList()
})
}
const handleSeriesSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl || !activeBrand.value) return
await formEl.validate(async (valid) => {
if (!valid) return
if (seriesDialogType.value === 'add') {
const { message } = await vehicleService.seriesAdd({
brand_id: activeBrand.value.id,
series_name: seriesForm.series_name,
description: seriesForm.description,
sort: seriesForm.sort,
status: seriesForm.status
})
ElMessage.success(message)
} else {
const { message } = await vehicleService.seriesEdit({
id: seriesForm.id,
brand_id: activeBrand.value.id,
series_name: seriesForm.series_name,
description: seriesForm.description,
sort: seriesForm.sort,
status: seriesForm.status
})
ElMessage.success(message)
}
seriesDialogVisible.value = false
getSeriesList()
})
}
// Logo上传处理
const handleLogoSuccess = (response: any) => {
brandForm.logo = response.url || (response.data && response.data.file_name) || ''
// 如需同步到 form.picture
logourl.value = response.url || (response.data && response.data.file_url) || ''
ElMessage.success('Logo上传成功')
}
const beforeLogoUpload = (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
}
onMounted(async () => {
await getBrandList()
})
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.dict-layout {
.dict-type-card,
.dict-item-card {
background: var(--el-bg-color);
padding: 12px;
border-radius: 8px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.left {
font-weight: 500;
}
.right {
display: flex;
align-items: center;
}
.muted {
color: var(--el-text-color-secondary);
}
}
.type-pagination {
margin-top: 8px;
text-align: center;
}
}
.brand-logo-cell {
display: flex;
justify-content: center;
align-items: center;
img {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.logo-placeholder {
width: 40px;
height: 40px;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
border-radius: 4px;
}
}
.logo-uploader {
:deep(.el-upload) {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
&:hover {
border-color: var(--el-color-primary);
}
}
}
.logo-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
}
.logo-preview {
width: 100px;
height: 100px;
display: block;
object-fit: cover;
}
.dialog-footer {
text-align: right;
}
</style>
+356
View File
@@ -0,0 +1,356 @@
<template>
<div class="color-page">
<div class="color-toolbar boxshadow">
<div class="color-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入颜色编码或名称"
clearable
@keyup.enter="getColorList"
/>
</el-form-item>
<el-form-item label="类型:" label-width="50px">
<el-select v-model="typeFilter" placeholder="全部类型" clearable style="min-width: 120px">
<el-option label="外观颜色" :value="1" />
<el-option label="内饰颜色" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态:" label-width="50px">
<el-select
v-model="statusFilter"
placeholder="全部状态"
clearable
style="min-width: 120px"
>
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getColorList" v-ripple>搜索</el-button>
<el-button @click="resetSearch" v-ripple>重置</el-button>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('color@add')" type="primary">
新增颜色
</ElButton>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="颜色编码" prop="color_code" min-width="120" />
<ElTableColumn label="颜色名称" prop="color_text" min-width="150" />
<ElTableColumn label="类型" prop="type" width="120">
<template #default="scope">
<ElTag :type="scope.row.type === 1 ? 'primary' : 'success'">
{{ scope.row.type === 1 ? '外观颜色' : '内饰颜色' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="排序" prop="sort" width="80" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">
{{ scope.row.status ? '启用' : '停用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('color@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteColor(scope.row)"
v-if="hasAuth('color@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增颜色' : '编辑颜色'"
align-center
width="600px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<ElFormItem label="颜色编码" prop="color_code" v-if="dialogType === 'edit'">
<ElInput v-model="form.color_code" disabled placeholder="请输入颜色编码" />
</ElFormItem>
<ElFormItem label="颜色名称" prop="color_text">
<ElInput v-model="form.color_text" placeholder="请输入颜色名称" />
</ElFormItem>
<ElFormItem label="类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio :label="1">外观颜色</el-radio>
<el-radio :label="2">内饰颜色</el-radio>
</el-radio-group>
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { vehicleService } from '@/api/vehicleApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminVehicleColor' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const typeFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
// 表单数据
const form = reactive<any>({
id: '',
color_code: '',
color_text: '',
type: 1,
status: 1,
sort: 0
})
// 表单验证规则
const rules = reactive<FormRules>({
color_text: [{ required: true, message: '请输入颜色名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择颜色类型', trigger: 'change' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getColorList()
})
// 监听搜索条件变化
watch([keyword, typeFilter, statusFilter], () => {
pagination.currentPage = 1
getColorList()
})
// 获取颜色列表
const getColorList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await vehicleService.getColorList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取颜色列表失败:', error)
ElMessage.error('获取颜色列表失败')
} finally {
loading.value = false
}
}
// 重置搜索条件
const resetSearch = () => {
keyword.value = ''
typeFilter.value = undefined
statusFilter.value = undefined
pagination.currentPage = 1
getColorList()
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getColorList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getColorList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式
Object.assign(form, {
id: row.id,
color_code: row.color_code,
color_text: row.color_text,
type: row.type,
status: row.status,
sort: row.sort || 0
})
} else {
// 新增模式
Object.assign(form, {
id: '',
color_code: '',
color_text: '',
type: 1,
status: 1,
sort: 0
})
}
}
// 删除颜色
const deleteColor = (row: any) => {
ElMessageBox.confirm('确定删除该颜色吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await vehicleService.colorDelete({ id: row.id })
ElMessage.success(message)
getColorList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
// 新增
const { message } = await vehicleService.colorAdd({
color_text: form.color_text,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
} else {
// 编辑
const { message } = await vehicleService.colorEdit({
id: form.id,
color_text: form.color_text,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
}
dialogVisible.value = false
getColorList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.color-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.color-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.color-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+356
View File
@@ -0,0 +1,356 @@
<template>
<div class="modification-page">
<div class="modification-toolbar boxshadow">
<div class="modification-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getModificationList"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getModificationList" v-ripple>搜索</el-button>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton
@click="showDialog('add')"
v-ripple
v-if="hasAuth('modification@add')"
type="primary"
>
新增改装项目
</ElButton>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="项目编码" prop="code" min-width="120" />
<ElTableColumn label="项目名称" prop="name" min-width="150" />
<ElTableColumn label="改装类型" prop="type" width="120">
<template #default="scope">
<ElTag :type="getTypeTagType(scope.row.type)">
{{ getTypeLabel(scope.row.type) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="排序" prop="sort" width="80" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">
{{ scope.row.status ? '启用' : '停用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button
type="danger"
link
@click="deleteModification(scope.row)"
v-if="hasAuth('modification@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增改装项目' : '编辑改装项目'"
align-center
width="600px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<ElFormItem label="项目编码" prop="code" v-if="dialogType === 'edit'">
<ElInput v-model="form.code" disabled placeholder="请输入改装项目编码" />
</ElFormItem>
<ElFormItem label="项目名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入改装项目名称" />
</ElFormItem>
<ElFormItem label="改装类型" prop="type">
<el-select v-model="form.type" placeholder="请选择改装类型" style="width: 100%">
<el-option label="外观改装" :value="1" />
<el-option label="性能改装" :value="2" />
<el-option label="内饰改装" :value="3" />
<el-option label="电子设备" :value="4" />
<el-option label="其他" :value="5" />
</el-select>
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { vehicleService } from '@/api/vehicleApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminVehicleModification' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const typeFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
// 表单数据
const form = reactive<any>({
id: '',
code: '',
name: '',
type: 1,
status: 1,
sort: 0
})
// 表单验证规则
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入改装项目名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择改装类型', trigger: 'change' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getModificationList()
})
// 监听搜索条件变化
watch([keyword, typeFilter, statusFilter], () => {
pagination.currentPage = 1
getModificationList()
})
// 获取改装项目列表
const getModificationList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await vehicleService.getModificationList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取改装项目列表失败:', error)
ElMessage.error('获取改装项目列表失败')
} finally {
loading.value = false
}
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getModificationList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getModificationList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式
Object.assign(form, {
id: row.id,
code: row.code,
name: row.name,
type: row.type,
status: row.status,
sort: row.sort || 0
})
} else {
// 新增模式
Object.assign(form, {
id: '',
code: '',
name: '',
type: 1,
status: 1,
sort: 0
})
}
}
// 删除改装项目
const deleteModification = (row: any) => {
ElMessageBox.confirm('确定删除该改装项目吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await vehicleService.modificationDelete({ id: row.id })
ElMessage.success(message)
getModificationList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
// 新增
const { message } = await vehicleService.modificationAdd({
name: form.name,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
} else {
// 编辑
const { message } = await vehicleService.modificationEdit({
id: form.id,
name: form.name,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
}
dialogVisible.value = false
getModificationList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
// 获取类型标签样式
const getTypeTagType = (type: number): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
const typeMap: Record<number, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
1: 'primary', // 外观改装
2: 'success', // 性能改装
3: 'warning', // 内饰改装
4: 'info', // 电子设备
5: 'danger' // 其他
}
return typeMap[type] || 'info'
}
// 获取类型标签文本
const getTypeLabel = (type: number) => {
const typeMap: Record<number, string> = {
1: '外观改装',
2: '性能改装',
3: '内饰改装',
4: '电子设备',
5: '其他'
}
return typeMap[type] || '未知类型'
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.modification-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.modification-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.modification-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+353
View File
@@ -0,0 +1,353 @@
<template>
<div class="port-page">
<div class="port-toolbar boxshadow">
<div class="toolbar-left">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('port@add')" type="primary"
>创建港口</ElButton
>
</div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getPortList"
>
<template #suffix>
<el-icon class="clickable" @click="getPortList"><Search /></el-icon>
</template>
</el-input>
</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"
>
<template #default>
<ElTableColumn label="港口代码" prop="port_code" min-width="120" />
<ElTableColumn label="港口名称" prop="port_name" min-width="150" />
<ElTableColumn label="国家" prop="country_id" min-width="140">
<template #default="scope">
<span>{{ scope.row.country_name }}</span>
</template>
</ElTableColumn>
<!-- <ElTableColumn label="纬度" prop="lat" min-width="100" /> -->
<!-- <ElTableColumn label="经度" prop="lng" min-width="100" /> -->
<ElTableColumn label="排序" prop="sort" width="80" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
scope.row.status ? '启用' : '停用'
}}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button link @click="showDialog('edit', scope.row)" v-if="hasAuth('port@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deletePort(scope.row)"
v-if="hasAuth('port@destroy')"
>删除</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增港口' : '编辑港口'"
align-center
width="720px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="form">
<ElFormItem label="港口代码" prop="port_code">
<ElInput v-model="form.port_code" placeholder="请输入港口代码" />
</ElFormItem>
<ElFormItem label="港口名称" prop="port_name">
<ElInput v-model="form.port_name" placeholder="请输入港口名称" />
</ElFormItem>
<ElFormItem label="国家" prop="country_id">
<el-select
v-model="form.country_id"
filterable
placeholder="请选择国家"
style="width: 100%"
>
<el-option
v-for="item in countryOptions"
:key="item.id"
:label="item.cn_addr"
:value="item.id"
/>
</el-select>
</ElFormItem>
<!-- <ElFormItem label="纬度" prop="lat">
<ElInput v-model="form.lat" placeholder="请输入纬度" />
</ElFormItem>
<ElFormItem label="经度" prop="lng">
<ElInput v-model="form.lng" placeholder="请输入经度" />
</ElFormItem> -->
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="港口描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
maxlength="300"
show-word-limit
placeholder="请输入港口描述"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { vehicleService } from '@/api/vehicleApi'
import { commonService } from '@/api/commonApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'AdminVehiclePort' })
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const countryFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
const countryOptions = ref<any[]>([])
const loadCountryOptions = async () => {
try {
const res = await commonService.getCountrySelect()
countryOptions.value = res || []
} catch (e) {
countryOptions.value = []
}
}
const form = reactive<any>({
id: '',
status: 1,
sort: 0,
port_code: '',
port_name: '',
lat: '',
lng: '',
description: '',
country_id: undefined
})
const rules = reactive<FormRules>({
port_code: [{ required: true, message: '请输入港口代码', trigger: 'blur' }],
port_name: [{ required: true, message: '请输入港口名称', trigger: 'blur' }],
country_id: [{ required: true, message: '请选择国家', trigger: 'change' }]
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableData = ref<any[]>([])
const tableRef = ref()
onMounted(() => {
loadCountryOptions()
getPortList()
})
watch([keyword, countryFilter, statusFilter], () => {
pagination.currentPage = 1
getPortList()
})
const getPortList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await vehicleService.getPortList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取港口列表失败:', error)
ElMessage.error('获取港口列表失败')
} finally {
loading.value = false
}
}
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getPortList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getPortList()
}
const showDialog = (type: 'add' | 'edit', row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
Object.assign(form, {
id: row.id,
status: row.status,
sort: row.sort ?? 0,
port_code: row.port_code,
port_name: row.port_name,
lat: row.lat,
lng: row.lng,
description: row.description,
country_id: row.country_id
})
} else {
Object.assign(form, {
id: '',
status: 1,
sort: 0,
port_code: '',
port_name: '',
lat: '',
lng: '',
description: '',
country_id: undefined
})
}
}
const deletePort = (row: any) => {
ElMessageBox.confirm('确定删除该港口吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await vehicleService.portDelete({ id: row.id })
ElMessage.success(message)
getPortList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
const { message } = await vehicleService.portAdd({
status: form.status,
sort: form.sort,
port_code: form.port_code,
port_name: form.port_name,
lat: form.lat,
lng: form.lng,
description: form.description,
country_id: form.country_id
})
ElMessage.success(message)
} else {
const { message } = await vehicleService.portEdit({
id: form.id,
status: form.status,
sort: form.sort,
port_code: form.port_code,
port_name: form.port_name,
lat: form.lat,
lng: form.lng,
description: form.description,
country_id: form.country_id
})
ElMessage.success(message)
}
dialogVisible.value = false
getPortList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
const getCountryLabel = (id: number | undefined) => {
if (!id) return '-'
const found = countryOptions.value.find((c: any) => c.id === id)
return found ? found.name : id
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.port-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.port-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.port-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -36,11 +36,11 @@
<div class="form">
<!-- 登录方式切换 tab -->
<div class="login-type-tabs">
<!-- <span
<span
:class="{ active: activeLoginType === 'password' }"
@click="activeLoginType = 'password'"
>账号密码登录</span
> -->
>
<span
:class="{ active: activeLoginType === 'qrcode' }"
@click="activeLoginType = 'qrcode'"
+362
View File
@@ -0,0 +1,362 @@
<template>
<div class="signature-page" :class="{ landscape }">
<div class="header">电子签名</div>
<div class="canvas-wrap">
<canvas
ref="canvasRef"
class="canvas"
:style="{ pointerEvents: locked ? 'none' : 'auto' }"
@touchstart.prevent="onStart"
@touchmove.prevent="onMove"
@touchend.prevent="onEnd"
@mousedown.prevent="onMouseDown"
@mousemove.prevent="onMouseMove"
@mouseup.prevent="onMouseUp"
></canvas>
</div>
<div class="toolbar">
<el-button :disabled="locked" @click="clearCanvas">清空</el-button>
<!-- <el-button :disabled="locked" @click="toggleLandscape">横屏</el-button> -->
<el-button type="primary" :disabled="locked" :loading="submitting" @click="saveSignature">
保存
</el-button>
</div>
<div class="tip">
{{ locked ? '签名已保存,页面已锁定' : '请在画布上手写签名,然后点击保存' }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
// import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
import { useUserStore } from '@/store/modules/user'
const route = useRoute()
const userId = route.query.userId as string | undefined
const userStore = useUserStore()
// 优先使用URL传入的token,否则取当前应用内的token
const urlToken =
(route.query.accessToken as string | undefined) || (route.query.token as string | undefined)
// 其他头部信息从URL透传(便于直达页调用接口)
const urlLang = route.query.lang as string | undefined
const urlCorp =
(route.query.companyabbr as string | undefined) ||
(route.query.corp as string | undefined) ||
userStore.companyabbr
const urlLoginType =
(route.query.loginType as string | undefined) || (route.query.type as string | undefined)
const urlSys =
(route.query.systemsabbr as string | undefined) ||
(route.query.sys as string | undefined) ||
userStore.systemsabbr ||
'fdesk'
const effectiveToken = urlToken || userStore.accessToken
// 从URL参数获取公司信息并存储到userStore(如果路由守卫没有处理)
const companyid =
(route.query.companyid as string | undefined) || (route.query.corpid as string | undefined)
const companyabbr =
(route.query.companyabbr as string | undefined) || (route.query.corp as string | undefined)
if (companyid && companyabbr && (!userStore.companyid || !userStore.companyabbr)) {
userStore.setCompany(companyid, companyabbr)
}
// 从URL参数获取系统信息并存储到userStore(如果路由守卫没有处理)
const systemsabbr =
(route.query.systemsabbr as string | undefined) || (route.query.sys as string | undefined)
const systemsid =
(route.query.systemsid as string | undefined) || (route.query.sysid as string | undefined)
if (systemsid && systemsabbr && (!userStore.systemsid || !userStore.systemsabbr)) {
userStore.setSystems(systemsid, systemsabbr)
}
// 从URL参数获取登录类型并存储到userStore(如果路由守卫没有处理)
if (urlLoginType && !userStore.loginType) {
userStore.setLoginType(urlLoginType as 'feishu' | 'upwd')
}
// 从URL参数获取token并存储到userStore(如果路由守卫没有处理)
if (urlToken && !userStore.accessToken) {
userStore.setToken(urlToken)
}
// 调试信息:打印初始化时的userStore状态
// console.log('Signature page initialized:', {
// urlParams: {
// userId,
// urlToken: urlToken ? 'exists' : 'missing',
// urlLang,
// urlCorp,
// urlLoginType,
// urlSys
// },
// userStore: {
// companyabbr: userStore.companyabbr,
// companyid: userStore.companyid,
// systemsabbr: userStore.systemsabbr,
// systemsid: userStore.systemsid,
// language: userStore.language,
// loginType: userStore.loginType,
// accessToken: userStore.accessToken ? 'exists' : 'missing',
// isLogin: userStore.isLogin
// }
// })
const { uploadHeaders } = useUploadHeaders()
// 基于URL参数动态构造上传头,优先使用URL透传值
const dynamicHeaders = computed(() => {
const headers: Record<string, string> = { ...(uploadHeaders.value || {}) }
// 语言设置
if (urlLang) headers['Fdesk-Language'] = urlLang
// 公司标识 - 优先使用URL参数,其次使用userStore,最后使用默认值
const corpAbbr = urlCorp || userStore.companyabbr || 'default'
headers['Fdesk-Corp-Abbr'] = corpAbbr
// 登录类型
if (urlLoginType) headers['Fdesk-Login-Type'] = urlLoginType
// 系统标识
headers['Fdesk-System-Code'] = urlSys
// 认证令牌
if (effectiveToken) headers['Fdesk-Auth-Token'] = 'Bearer ' + effectiveToken
// 调试信息:打印请求头
// console.log('Signature page headers:', headers)
// console.log('URL params:', {
// urlLang,
// urlCorp,
// urlLoginType,
// urlSys,
// effectiveToken
// })
// console.log('Final corp abbr:', corpAbbr)
return headers
})
const canvasRef = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasRenderingContext2D | null>(null)
const drawing = ref(false)
const submitting = ref(false)
const landscape = ref(false)
const locked = ref(false)
const resizeCanvas = () => {
const canvas = canvasRef.value
if (!canvas) return
const dpr = window.devicePixelRatio || 1
let width = Math.min(window.innerWidth - 32, 800)
let height = Math.round(width * 0.5)
if (landscape.value && window.innerHeight > 0) {
// 横屏时尽量占满高度
const h = Math.min(window.innerHeight - 120, 500)
height = h
width = Math.round(h * 2)
}
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
canvas.width = Math.floor(width * dpr)
canvas.height = Math.floor(height * dpr)
const c = canvas.getContext('2d')!
c.scale(dpr, dpr)
c.lineWidth = 3
c.lineCap = 'round'
c.lineJoin = 'round'
c.strokeStyle = '#111'
ctx.value = c
// 背景白色
c.fillStyle = '#fff'
c.fillRect(0, 0, width, height)
}
onMounted(() => {
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
})
const getPoint = (e: TouchEvent | MouseEvent) => {
const canvas = canvasRef.value!
const rect = canvas.getBoundingClientRect()
let clientX = 0
let clientY = 0
if (e instanceof TouchEvent) {
const t = e.touches[0] || e.changedTouches[0]
clientX = t.clientX
clientY = t.clientY
} else {
clientX = (e as MouseEvent).clientX
clientY = (e as MouseEvent).clientY
}
return { x: clientX - rect.left, y: clientY - rect.top }
}
const onStart = (e: TouchEvent) => {
if (locked.value) return
drawing.value = true
const p = getPoint(e)
ctx.value!.beginPath()
ctx.value!.moveTo(p.x, p.y)
}
const onMove = (e: TouchEvent) => {
if (locked.value) return
if (!drawing.value) return
const p = getPoint(e)
ctx.value!.lineTo(p.x, p.y)
ctx.value!.stroke()
}
const onEnd = () => {
if (locked.value) return
drawing.value = false
}
const onMouseDown = (e: MouseEvent) => onStart(e as any)
const onMouseMove = (e: MouseEvent) => onMove(e as any)
const onMouseUp = () => onEnd()
const clearCanvas = () => {
if (locked.value) return
resizeCanvas()
}
const dataURLToFile = (dataURL: string, filename: string) => {
const arr = dataURL.split(',')
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png'
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) u8arr[n] = bstr.charCodeAt(n)
return new File([u8arr], filename, { type: mime })
}
const uploadUrl = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=user_signature`
const requestFullscreen = async (el: HTMLElement) => {
const anyEl = el as any
if (anyEl.requestFullscreen) return anyEl.requestFullscreen()
if (anyEl.webkitRequestFullscreen) return anyEl.webkitRequestFullscreen()
if (anyEl.msRequestFullscreen) return anyEl.msRequestFullscreen()
}
const lockOrientationLandscape = async () => {
const anyScr = screen as any
if (anyScr.orientation && anyScr.orientation.lock) {
try {
await anyScr.orientation.lock('landscape')
} catch {
// ignore
}
}
}
const isLimitedWebview = () => /MicroMessenger|Lark|Feishu|DingTalk/i.test(navigator.userAgent)
const toggleLandscape = async () => {
landscape.value = !landscape.value
if (landscape.value) {
// 在微信/飞书等内置浏览器中,强制横屏普遍受限,采用 CSS 旋转兜底
if (!isLimitedWebview()) {
await requestFullscreen(document.documentElement)
await lockOrientationLandscape()
}
}
setTimeout(resizeCanvas, 200)
}
const saveSignature = async () => {
if (!userId) {
ElMessage.error('缺少用户信息')
return
}
try {
submitting.value = true
const canvas = canvasRef.value!
const dataUrl = canvas.toDataURL('image/png')
const file = dataURLToFile(dataUrl, `signature_${userId}_${Date.now()}.png`)
const form = new FormData()
form.append('file', file)
const headers: Record<string, string> = { ...(dynamicHeaders.value || {}) }
// 兼容后端可能验 Authorization 的情况
if (effectiveToken) headers['Authorization'] = `Bearer ${effectiveToken}`
// 调试信息:打印上传请求详情
// console.log('Upload URL:', uploadUrl)
// console.log('Upload headers:', headers)
// console.log('UserStore state:', {
// systemsabbr: userStore.systemsabbr,
// systemsid: userStore.systemsid,
// companyabbr: userStore.companyabbr,
// companyid: userStore.companyid,
// language: userStore.language,
// loginType: userStore.loginType
// })
const res = await fetch(uploadUrl, {
method: 'POST',
headers,
body: form
})
const json = await res.json()
// console.log('Upload response:', json)
if (json.code !== 200) throw new Error(json.message || '上传失败')
// 将签名保存到个人信息(假设 crmuseredit 接受 signature 和 signature_pic_full 字段)
const res1 = await commonService.signatureEdit({
sign_img: json.data.file_name
})
ElMessage.success(res1?.message || '签名已保存')
locked.value = true
// 完成后可提示返回或自动关闭
// setTimeout(() => {
// if (window.history.length > 1) window.history.back()
// }, 800)
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.signature-page {
padding: 16px;
}
.header {
text-align: center;
font-weight: 600;
font-size: 18px;
margin-bottom: 12px;
}
.canvas-wrap {
display: flex;
justify-content: center;
}
.canvas {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 6px;
touch-action: none;
}
.toolbar {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 12px;
}
.tip {
text-align: center;
color: #909399;
margin-top: 8px;
font-size: 12px;
}
</style>
+276
View File
@@ -0,0 +1,276 @@
<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="readfnpl" :disabled="selectedRows.length === 0">
<i class="iconfont-sys">&#xe62c;</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"
>
<template #default>
<el-table-column type="selection" width="55px" align="center"></el-table-column>
<el-table-column prop="id" label="编号" align="center" width="240px">
<template #default="scope">
<span>
{{ scope.row.message.msg_no }}
</span>
</template>
</el-table-column>
<el-table-column prop="id" label="通知标题" align="center">
<template #default="scope">
<span>
{{ scope.row.message.title }}
</span>
</template>
</el-table-column>
<el-table-column prop="id" label="通知内容" align="center">
<template #default="scope">
<span v-html="scope.row.message.content" style="white-space: pre-line"></span>
</template>
</el-table-column>
<el-table-column prop="id" label="发送人" align="center">
<template #default="scope">
<span>
{{ scope.row.message.sender_name }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" align="center">
<template #default="scope">
<el-text class="mx-1" :type="scope.row.status === 0 ? 'info' : 'success'">
{{ scope.row.status === 1 ? '已读' : '未读' }}
</el-text>
</template>
</el-table-column>
<el-table-column prop="id" label="创建时间" align="center">
<template #default="scope">
<span>
{{ scope.row.message.created_at }}
</span>
</template>
</el-table-column>
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button link @click="showfn(scope.row)"> 详情 </el-button>
<!-- <el-button type="success" link @click="readfn(scope.row)"> 已读 </el-button> -->
<el-button type="danger" link @click="deleteRole(scope.row)"> 删除 </el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog v-model="dialogVisible" title="通知详情" align-center width="640px">
<div class="detail-dialog">
<div class="section">
<div class="info-grid">
<div class="label">发送人</div>
<div class="value">{{ noticeDetail.sender_name || '-' }}</div>
</div>
<div class="info-grid">
<div class="label">通知标题</div>
<div class="value">{{ noticeDetail.title || '-' }}</div>
</div>
</div>
<div class="section">
<div class="label" style="margin-bottom: 8px">通知内容</div>
<div class="content-box">
<div class="message-text" v-html="noticeDetail.content"></div>
</div>
</div>
</div>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { commonService } from '@/api/commonApi'
// import { useAuth } from '@/composables/useAuth'
// const { hasAuth } = useAuth()
defineOptions({ name: 'Type ' })
const loading = ref(false)
const dialogVisible = ref(false)
const keyword = ref<string>('')
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
watch([keyword], () => {
pagination.currentPage = 1
getWorkList()
})
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 选中的行数据
const selectedRows = ref<any[]>([])
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await commonService.getNoticeList({
current: currentPage,
size: pageSize
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const noticeDetail = ref<any>({})
const showfn = async (row: any) => {
dialogVisible.value = true
await commonService.getNoticeDetail({ id: row.id }).then((res: any) => {
noticeDetail.value = res.message
console.log(res)
})
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await commonService.delNotice({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const readfnpl = async () => {
ElMessageBox.confirm('确定已读吗?', '确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await commonService.readNotice({
msg_ids: selectedRows.value
})
ElMessage.success(message)
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;
}
.message-text {
padding: 10px 14px;
line-height: 1.7;
white-space: pre-line;
word-break: break-word;
}
.detail-dialog {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 12px 14px;
}
.info-grid {
display: grid;
grid-template-columns: 96px 1fr;
gap: 6px 12px;
align-items: center;
padding: 6px 0;
}
.label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.value {
color: var(--el-text-color-primary);
font-size: 14px;
}
.content-box {
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
max-height: 320px;
overflow: auto;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.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;
}
</style>
+554
View File
@@ -0,0 +1,554 @@
<template>
<div class="stocking-show-page">
<div class="boxshadow">
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">审批记录</span>
</div>
<div class="approval-timeline">
<div v-for="(rec, idx) in approvalRecords" :key="idx" class="timeline-item">
<div class="left">
<div class="avatar-box">
<span class="initial">{{ rec.name?.slice(0, 1) || '—' }}</span>
<i v-if="rec.approved" class="status-dot iconfont-sys">&#xe621;</i>
<i v-if="rec.rejected" class="status-dot rejected">&#xe83a;</i>
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys">&#xe839;</i>
</div>
</div>
<div class="right">
<div class="row name-role">
<span class="name">{{ rec.name }}</span>
<span class="role">({{ rec.role || rec.type || '审批人' }})</span>
</div>
<div class="row time">{{ rec.time }}</div>
</div>
</div>
<div v-if="!approvalRecords.length" class="empty">暂无审批记录</div>
</div>
</div>
<div class="boxshadow" v-if="status == 'pending'">
<div class="form-title">
<span class="border"></span>
<span class="title">审批</span>
</div>
<div class="">
<ElRow :gutter="12">
<ElCol :span="24">
<ElFormItem label="备注:">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="请输入备注信息"
class="approval-notes"
/>
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<div class="form-card" v-if="status == 'pending'">
<!-- 底部信息 -->
<div class="submit-wrap">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleSubmit('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
>同意</ElButton
>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
// import { manageService } from '@/api/adminApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
import { router } from '@/router'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'CrmApprovalDetail' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { pdf, frameRef, download } = usePdfPreview()
const route = useRoute()
const orderDialog = reactive({
orderData: {} as OrderData
})
// const approval_id = route.query.id
const status = route.query.status
const stocking_id = route.query.biz_id
const instance_id = route.query.instance_id
const stockingInfo = ref<any>({})
const approvalNotes = ref('')
const approvalAction = ref('')
const handleSubmit = async (approvalAction: string) => {
try {
// 这里调用审批API
const { message } = await CrmService.stockapprovalpass({
instance_id: instance_id,
action: approvalAction,
comment: approvalNotes.value
})
router.push('/crmpc/approval/stocking_auth')
ElMessage.success(message)
} catch {
ElMessage.error('提交失败')
}
}
// 审批记录(先填充假数据展示)
const approvalRecords = ref<any[]>([])
const getDictItemSelectlist = ref<any[]>([])
const getDictItemSelect = async () => {
const res = await commonService.getDictItemSelect({ dict_id: '18' })
getDictItemSelectlist.value = res
}
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.checkStock({ id: stocking_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
stockingInfo.value = res
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
// 审批记录(兼容多种字段)
const resstockapprovallogs = await CrmService.stockapprovallogs({ instance_id: instance_id })
console.log(resstockapprovallogs)
const historyList = resstockapprovallogs
if (historyList.length > 0) {
approvalRecords.value = historyList.map((h: any) => ({
name: h?.initiator_name ? h?.initiator_name : h?.name,
role: h?.index == -1 ? '申请人' : '审批人',
time: h?.finished_at || '',
approved: h?.status === 'approved' || h?.status === 'submitted',
rejected: h?.status === 'rejected',
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted'
}))
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getDictItemSelect()
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page {
min-height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 {
margin-right: 40px;
}
}
.form-title {
font-weight: bold;
color: #222;
margin-bottom: 15px;
}
.form-title .title {
margin-left: 6px;
}
.items-wrap {
margin-top: 6px;
}
.item-row {
padding: 8px 0;
}
:deep(.el-input.is-disabled .el-input__inner) {
color: var(--el-text-color-regular);
}
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
.submit-wrap {
display: flex;
justify-content: center;
padding: 24px 0 8px;
}
/* 审批记录 时间线 */
.approval-timeline {
padding: 8px 4px 8px 0;
}
.timeline-item {
display: flex;
align-items: flex-start;
gap: 10px;
position: relative;
}
.timeline-item + .timeline-item {
margin-top: 50px;
}
.left {
width: 46px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.avatar-box {
width: 46px;
height: 46px;
background: #3d7bff;
border-radius: 10px;
color: #fff;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 20px;
}
.status-dot {
position: absolute;
right: -4px;
bottom: -4px;
// width: 14px;
// height: 14px;
font-size: 9px;
padding: 3px 1px;
border-radius: 50%;
background: #22c55e;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
}
.status-dot.pending {
background: #3471ff;
padding: 3px 3px;
}
.status-dot.rejected {
background: #ff0000;
padding: 3px 3px;
}
/* 连接下一个节点的虚线 */
.timeline-item:not(:last-child) .left::after {
content: '';
position: absolute;
top: 52px; /* avatar 46px + 间距 6px */
left: 23px; /* 盒子宽度一半,确保竖线居中 */
width: 0;
height: calc(100% + 16px); /* 加上相邻项之间的间距 */
border-left: 2px dotted #bfbfbf;
}
.right {
flex: 1;
padding-top: 2px;
}
.row.name-role {
color: #222;
}
.name {
font-weight: 600;
}
.role {
color: #666;
margin-left: 4px;
}
.time {
color: #888;
font-size: 12px;
margin-top: 2px;
}
.empty {
color: #999;
padding: 8px 0 4px 4px;
}
.submit-wrap {
display: flex;
justify-content: space-around;
padding: 24px 0 8px;
width: 500px;
margin: auto;
}
.submit-btn {
width: 100px;
height: 44px;
font-size: 16px;
}
</style>
+384
View File
@@ -0,0 +1,384 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<!-- <div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div> -->
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</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"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120" />
<ElTableColumn label="审核人" prop="approver_name" min-width="120" />
<ElTableColumn label="审批时间" prop="approved_at" min-width="120" />
<ElTableColumn label="状态" prop="status_desc" min-width="120" />
<ElTableColumn label="创建时间" prop="created_at" min-width="120" />
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
type="primary"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看审批
</el-link>
<el-link
v-if="scope.row.status == 'pending'"
type="success"
underline="never"
@click="handleCreateRepair(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 快速审批
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 快速审批弹出框 -->
<el-dialog
v-model="approvalDialogVisible"
title="合塑实业-汽车事业部-备货单(客户下定)"
width="80%"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="approval-dialog"
>
<div class="approval-content">
<!-- 头部信息 -->
<OrderDetailContent :orderData="orderDialog.orderData" />
<!-- 审批备注 -->
<div class="info-section">
<ElRow :gutter="12">
<ElCol :span="24">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
</ElCol>
</ElRow>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleApprove('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
>同意</ElButton
>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
// import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'CrmStockingAuth' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { hasAuth } = useAuth()
const orderDialog = reactive({
orderData: {} as OrderData
})
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// 审批弹出框相关
const approvalDialogVisible = ref(false)
const approvalNotes = ref('')
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await CrmService.stockapprovalrecord(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const handleView = (row: any) => {
router.push({
path: '/crmpc/approval/auth_detil',
query: { id: row.id, biz_id: row.biz_id, instance_id: row.instance_id, status: row.status } // 可带客户名等参数
})
}
const getDictItemSelectlist = ref<any[]>([])
const getDictItemSelect = async () => {
const res = await commonService.getDictItemSelect({ dict_id: '18' })
getDictItemSelectlist.value = res
}
const instance_id = ref('')
const handleCreateRepair = async (row: any) => {
// getDictItemSelect()
instance_id.value = row.instance_id
const res = await CrmService.checkStock({ id: row.biz_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
approvalDialogVisible.value = true
}
// 审批操作
const handleApprove = async (approvalAction: string) => {
try {
// 这里调用审批API
const { message } = await CrmService.stockapprovalpass({
instance_id: instance_id.value,
action: approvalAction,
comment: approvalNotes.value
})
ElMessage.success(message)
approvalDialogVisible.value = false
approvalNotes.value = ''
instance_id.value = ''
// 刷新列表
getList()
} catch (error) {
console.error('审批失败:', error)
ElMessage.error('审批失败')
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
// 审批弹出框样式
.approval-dialog {
.approval-content {
max-height: 70vh;
overflow-y: auto;
}
.header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
color: #333333;
.mr40 { margin-right: 40px; }
}
.info-section {
margin-bottom: 24px;
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
.title-bar {
width: 4px;
height: 20px;
background: #409eff;
margin-right: 8px;
border-radius: 2px;
}
span {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
}
.order-items {
.order-item {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
.item-header {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
.item-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.item-details {
.detail-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 12px;
.detail-item {
display: flex;
flex-direction: column;
label {
margin-bottom: 6px;
font-size: 14px;
font-weight: 500;
color: #606266;
}
}
}
}
}
}
.summary-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f0f9ff;
border-radius: 8px;
margin-bottom: 24px;
.summary-item {
.summary-label {
font-weight: 600;
color: #606266;
margin-right: 8px;
}
.summary-value {
font-size: 18px;
font-weight: 700;
color: #409eff;
}
}
}
.approval-notes {
width: 100%;
}
.dialog-footer {
justify-content: space-around;
width: 500px;
margin: auto;
display: flex;
gap: 12px;
.reject-btn {
border-color: #409eff;
color: #409eff;
background: white;
&:hover {
background: #ecf5ff;
}
}
.approve-btn {
background: #409eff;
border-color: #409eff;
}
}
}
</style>
+32 -10
View File
@@ -46,8 +46,8 @@
<el-radio label="2">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系方式" />
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="Email" prop="email">
<el-input v-model="form.email" placeholder="请输入Email" />
@@ -81,12 +81,7 @@
</el-select>
</el-form-item>
<el-form-item label="公司" prop="company">
<el-input
v-model="form.company"
placeholder="请输入公司"
@click="showMapDialog = true"
readonly
/>
<el-input v-model="form.company" placeholder="请输入公司" />
</el-form-item>
<el-form-item label="备注" prop="describe">
<el-input v-model="form.describe" type="textarea" :rows="3" placeholder="请输入备注" />
@@ -109,6 +104,17 @@
<el-button type="primary" @click="confirmMapSelect">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="centerDialogVisible" title="提示" width="500" center>
<div style="text-align: center">
<span v-html="warningMsg"></span>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="centerDialogVisible = false">取消</el-button>
<el-button type="primary" @click="distinctNoticefn"> 通知跟进 </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
@@ -116,7 +122,9 @@
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { CrmService } from '@/api/crmApi'
defineOptions({ name: 'CustomerAdd' })
const warningMsg = ref('')
const centerDialogVisible = ref(false)
const router = useRouter()
const formRef = ref()
const form = ref({
@@ -172,12 +180,18 @@
function goBack() {
router.back()
}
const customer_id = ref('')
function submitForm() {
formRef.value.validate(async (valid: boolean) => {
if (valid) {
try {
const res = await CrmService.customerAdd(form.value)
if (res.status == 'distinct') {
customer_id.value = res.data.id
warningMsg.value = res?.message
centerDialogVisible.value = true
return
}
ElMessage.success(res?.message || '提交成功!')
router.push('/crmpc/customer')
} catch (e: any) {
@@ -186,6 +200,14 @@
}
})
}
const distinctNoticefn = async () => {
const res = await CrmService.repeatCustomer({
customer_id: customer_id.value
})
ElMessage.success(res?.message)
centerDialogVisible.value = false
router.push('/crmpc/customer')
}
const showMapDialog = ref(false)
const selectedAddress = ref('')
+3 -2
View File
@@ -46,8 +46,8 @@
<el-radio :label="2">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系方式" />
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="Email" prop="email">
<el-input v-model="form.email" placeholder="请输入Email" />
@@ -104,6 +104,7 @@
const customerId = route.query.id
const router = useRouter()
const formRef = ref()
defineOptions({ name: 'CustomerEdit' })
const rules = {
channel: [{ required: true, message: '请选择客户渠道', trigger: 'change' }],
+159 -15
View File
@@ -70,15 +70,21 @@
<el-card v-if="form.type == '1'" class="info-card" shadow="never" style="margin-top: 16px">
<div class="info-top">
<span class="edit-title">上传跟进截图/聊天截图</span>
<el-upload
class="upload-demo"
:action="actionImageUplode"
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:show-file-list="false"
>
<img :src="imageUrl" class="avatar" />
</el-upload>
<div class="upload-area" @paste="handlePaste" @click="triggerFileInput">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<div v-else class="upload-placeholder">
<i class="el-icon-upload"></i>
<p>点击上传或直接粘贴截图</p>
</div>
</div>
<!-- 隐藏的文件输入框,用于点击上传 -->
<input
ref="fileInput"
type="file"
accept="image/*"
style="display: none"
@change="handleFileChange"
/>
</div>
</el-card>
<el-card v-if="form.type == '1'" class="info-card" shadow="never" style="margin-top: 16px">
@@ -196,7 +202,7 @@
v-for="item in remindOptions.filter((opt) => opt.value !== 'custom')"
:key="item.value"
:class="['remind-btn', { active: remindValue === item.value }]"
@click="remindValue = item.value"
@click="selectPreset(item.value)"
>
{{ item.label }}
</div>
@@ -208,6 +214,10 @@
style="width: 100%"
CircleClose
:disabled-date="disabledPastDate"
@focus="selectCustom"
@change="selectCustom"
clearable
@clear="customDate = ''"
/>
<el-button type="primary" class="confirm-btn" @click="handleSuccessConfirm">确定</el-button>
</div>
@@ -233,6 +243,7 @@
const levelOptions = ref<any>()
const followDesc = ref('')
const activecolor = '#3471FF'
defineOptions({ name: 'CustomerFollowAdd' })
const successDialogVisible = ref(false) // 控制弹窗显示
const remindValue = ref('1')
@@ -340,6 +351,14 @@
ElMessage.error(e?.message || '提交失败')
}
}
// 提醒方式互斥处理
const selectPreset = (val: string) => {
remindValue.value = val
customDate.value = ''
}
const selectCustom = () => {
remindValue.value = 'custom'
}
// 在 <script setup> 里添加获取地理位置方法
function getCurrentLocation() {
if (navigator.geolocation) {
@@ -383,6 +402,72 @@
// 如需同步到 form.picture
form.value.picture = response.url || (response.data && response.data.file_name) || ''
}
// 处理粘贴上传
const handlePaste = async (event: ClipboardEvent) => {
event.preventDefault()
const items = event.clipboardData?.items
if (!items) return
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.type.indexOf('image') !== -1) {
const file = item.getAsFile()
if (file) {
await uploadImageFile(file)
}
break
}
}
}
// 触发文件选择
const triggerFileInput = () => {
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
if (fileInput) {
fileInput.click()
}
}
// 处理文件选择
const handleFileChange = async (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
await uploadImageFile(file)
}
// 清空input值,允许重复选择同一文件
target.value = ''
}
// 上传图片文件
const uploadImageFile = async (file: File) => {
try {
// 创建FormData
const formData = new FormData()
formData.append('file', file)
// 使用fetch上传到服务器
const response = await fetch(actionImageUplode, {
method: 'POST',
headers: uploadHeaders.value,
body: formData
})
const result = await response.json()
if (response.ok) {
// 上传成功,调用原有的成功处理逻辑
handleUploadSuccess(result)
ElMessage.success('图片上传成功')
} else {
ElMessage.error(result.message || '图片上传失败')
}
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('图片上传失败')
}
}
</script>
<style scoped>
@@ -529,16 +614,75 @@
text-align: center;
flex: 1;
}
.remind-btn:hover {
background: #3471ff;
color: #fff;
}
.remind-btn.active {
background: #3471ff;
color: #fff;
border: 1px solid #3471ff;
}
.confirm-btn {
.custom-date {
margin-top: 20px;
}
.custom-date .el-date-picker {
width: 100%;
height: 44px;
font-size: 18px;
}
/* 上传区域样式 */
.upload-area {
position: relative;
width: 120px;
height: 120px;
/* border: 2px dashed #d9d9d9; */
/* border-radius: 8px; */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
overflow: hidden;
}
.upload-area:hover {
border-color: #3471ff;
background-color: #f4f7fe;
}
.upload-area .avatar {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
margin-top: 16px;
}
.upload-placeholder {
text-align: center;
color: #999;
}
.upload-placeholder i {
font-size: 24px;
margin-bottom: 8px;
display: block;
}
.upload-placeholder p {
font-size: 12px;
margin: 0;
line-height: 1.4;
}
/* 原有的上传样式保留 */
.upload-demo {
width: 120px;
height: 120px;
}
.upload-demo .avatar {
width: 120px;
height: 120px;
border-radius: 8px;
object-fit: cover;
}
</style>
+2 -1
View File
@@ -20,7 +20,7 @@
<span>渠道:{{ customerInfo.channel_name }}</span>
</el-col>
<el-col :sm="24" :md="6" :lg="5">
<span>联系方式:{{ customerInfo.phone }}</span>
<span>电话:{{ customerInfo.phone }}</span>
</el-col>
<el-col :sm="24" :md="6" :lg="4">
<span>客户偏好:{{ customerInfo.car_type_name }}</span>
@@ -159,6 +159,7 @@
import emptyimg from '@/assets/img/crm/empty.png'
import { CrmService } from '@/api/crmApi'
import { ElMessage } from 'element-plus'
defineOptions({ name: 'CustomerShow' })
// 常量配置
const TIMELINE_PLACEMENT = 'top'
const router = useRouter()
+735
View File
@@ -0,0 +1,735 @@
<template>
<div class="customer-page">
<!-- 顶部操作栏 -->
<div class="customer-toolbar boxshadow">
<el-button type="primary" @click="goAdd" v-if="hasAuth('crm_customer@add')">
<el-icon><Plus /> </el-icon>
添加
</el-button>
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input v-model="searchValue" placeholder="请输入客户名称或电话" />
</el-form-item>
<el-form-item label="国家" label-width="70px">
<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 class="customer-tags boxshadow">
<!-- 前3组 -->
<div class="filter-row" v-for="item in visibleTagGroups" :key="item.id">
<span class="filter-label">{{ item.name }}</span>
<div class="filter-tags">
<span
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
@click="handleTagClick(item.id, '')"
>全部</span
>
<span
class="tag-items"
v-for="item1 in item.tags"
:key="item1.id"
@click="handleTagClick(item.id, item1.id)"
>
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
{{ item1.name }}
</span>
<el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge>
</span>
</div>
</div>
<!-- 更多 -->
<transition name="fade">
<div v-if="showMore">
<div class="filter-row" v-for="item in moreTagGroups" :key="item.id">
<span class="filter-label">{{ item.name }}</span>
<div class="filter-tags">
<span
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
@click="handleTagClick(item.id, '')"
>全部</span
>
<span
class="tag-items"
v-for="item1 in item.tags"
:key="item1.id"
@click="handleTagClick(item.id, item1.id)"
>
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
{{ item1.name }}
</span>
<el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge>
</span>
</div>
</div>
</div>
</transition>
<div class="filter-more-btn" v-if="moreTagGroups.length">
<el-link underline="never" @click="showMore = !showMore">
{{ showMore ? '收起' : '更多' }}
<i v-if="showMore" class="iconfont-sys"> &#xe864;</i>
<i v-else class="iconfont-sys">&#xe867;</i>
</el-link>
</div>
</div>
<div class="boxshadow">
<!-- Tab切换 -->
<div class="customer-toolbar">
<el-tabs v-model="activeTab" class="customer-tabs" style="flex: 1">
<el-tab-pane label="全部" name=""></el-tab-pane>
<el-tab-pane label="跟进中" name="0"></el-tab-pane>
<el-tab-pane label="已成交" name="1"></el-tab-pane>
<el-tab-pane label="已丢失" name="2"></el-tab-pane>
</el-tabs>
<div>
<el-form-item label="" label-width="70px">
<el-cascader :options="options" clearable v-model="shots" />
<!-- <el-select v-model="selectValue" placeholder="排序方式" style="width: 120px">
<el-option v-for="v in sortList" :key="v.value" :label="v.label" :value="v.value">
</el-option>
</el-select> -->
</el-form-item>
</div>
</div>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
lass="customer-table"
>
<template #default>
<el-table-column
prop="code"
label="编号"
align="center"
width="170px"
show-overflow-tooltip
/>
<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="follow" label="联系方式" width="240px" show-overflow-tooltip>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<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="follow_last" label="上次跟进"></el-table-column>
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column prop="describe" label="备注" align="center" />
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
<el-link
type="success"
underline="never"
style="margin-right: 8px"
@click="onDetail(scope.row)"
v-if="hasAuth('crm_customer@show')"
>
<i class="iconfont-sys"> &#xe70f;</i>详情
</el-link>
<el-link
type="primary"
underline="never"
style="margin-right: 8px"
@click="goEdit(scope.row)"
v-if="hasAuth('crm_customer@show')"
>
<i class="iconfont-sys"> &#xe70f;</i>查看
</el-link>
<el-link
type="primary"
underline="never"
style="margin-right: 8px"
@click="goAddFollow(scope.row)"
v-if="hasAuth('crm_customer@follow_add')"
>
<i class="iconfont-sys">&#xe6bf;</i> 跟进
</el-link>
<el-link
type="warning"
underline="never"
@click="reportfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe701;</i> 标记无效
</el-link>
<el-link
type="danger"
underline="never"
@click="delectfn(scope.row)"
style="margin-right: 8px"
v-if="hasAuth('crm_customer@remove')"
>
<i class="iconfont-sys">&#xe893;</i> 移除
</el-link>
<el-link
type="danger"
underline="never"
@click="crmdelectfn(scope.row)"
v-if="hasAuth('crm_customer@delete')"
>
<i class="iconfont-sys">&#xe6d4;</i> 删除
</el-link>
</template>
</el-table-column>
</template>
</ArtTable>
</div>
</div>
<el-dialog v-model="detailDialogVisible" title="详情">
<div class="detail-info-table">
<div class="row">
<div>客户名称:{{ detailRow.contacts }}</div>
<div>电话:{{ detailRow.phone }}</div>
<div>国家:{{ detailRow.country_name }}</div>
</div>
<div class="row">
<div>渠道:{{ detailRow.channel_name }}</div>
<div>邮箱:{{ detailRow.email }}</div>
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
</div>
<div class="row">
<div>发布人:{{ detailRow.enter_user_name }}</div>
<div>跟单人:{{ detailRow.user_name }}</div>
<div>录单时间:{{ detailRow.created_at }}</div>
</div>
<div class="row">
<div>
等级:
<span>
{{ detailRow.magnitude_txt }}
</span>
</div>
<div>
状态:<span>{{ detailRow.grade_txt }}</span>
</div>
<div>
跟进状态:<span>{{ detailRow.follow_txt }}</span>
</div>
</div>
<div class="row">
<div>备注:{{ detailRow.describe }}</div>
</div>
</div>
<div v-if="detailRow.desc_collect != '' && detailRow.desc_collect != 'null'" class="dtitle"
>详情内容</div
>
<div
v-if="detailRow.desc_collect != '' && detailRow.desc_collect != 'null'"
v-html="detailRow.desc_collect"
class="detils-content"
></div>
</el-dialog>
<el-dialog v-model="reportDialogVisible" title="标记无效" width="600px">
<el-form :model="reportForm" label-width="80px">
<el-form-item label="描述">
<el-input
v-model="reportForm.desc"
type="textarea"
:rows="4"
placeholder="请输入无效原因或说明"
/>
</el-form-item>
<el-form-item label="附件">
<el-upload
v-model:file-list="reportFileList"
:headers="uploadHeaders"
:action="reportUploadAction"
multiple
:limit="9"
:on-success="handleReportUploadSuccess"
:on-exceed="handleReportExceed"
:on-remove="removeReportFile"
>
<el-button type="primary">上传附件</el-button>
<!-- <template #tip>
<div class="el-upload__tip">支持图片、视频或文件</div>
</template> -->
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="reportDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitReport">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { hasAuth } = useAuth()
const searchValue = ref('')
const selectValue = ref('')
const activeTab = ref('')
const country_id = ref('')
const showMore = ref(false)
const loading = ref(false)
const detailDialogVisible = ref(false)
const detailRow = ref<any>(null)
const reportDialogVisible = ref(false)
const reportForm = reactive({ id: '', desc: '' })
const reportFileList = ref<any[]>([])
const { uploadHeaders } = useUploadHeaders()
const reportUploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload`
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const selectOptions = [
{ label: '全部', value: '' },
{ label: '当天', value: '0' },
{ label: '近三天', value: '1' },
{ label: '近七天', value: '2' }
]
const shots = ref([])
const options = ref<
Array<{
label: string
value: string
children: Array<{ label: string; value: string }>
}>
>([
{ label: '默认排序', value: 'id', children: [] },
{ label: '最后跟进', value: 'follow_last', children: [] },
{ label: '下次跟进', value: 'follow_next', children: [] },
{ label: '标签组', value: 'tag_group', children: [] }
])
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 }>({})
//获取标签
type TagGroup = { id: string; name: string; tags: { id: string; name: string; count: number }[] }
const tagGroups = ref<TagGroup[]>([])
const tagGrouplistfn = async () => {
const res = await CrmService.tagGrouplist({ scene: 'customers' })
tagGroups.value = res || []
tagGroups.value.forEach((group) => {
if (group.tags && Array.isArray(group.tags)) {
tagActiveMap[group.id] = ''
}
})
options.value[3].children = tagGroups.value.map((group) => ({
label: group.name,
value: group.id
}))
}
// 计算前3组和更多组
// eslint-disable-next-line vue/valid-define-props
defineProps()
const visibleTagGroups = computed(() => tagGroups.value.slice(0, 3))
const moreTagGroups = computed(() => tagGroups.value.slice(3))
// 表格数据
const tableData = ref<any[]>([])
onMounted(() => {
getarea()
gettableList()
tagGrouplistfn()
})
// tabs 切换联动
watch([activeTab, searchValue, selectValue, country_id, shots], () => {
console.log('tabs 联动' + shots.value)
pagination.currentPage = 1
gettableList()
})
const gettableList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize
}
if (searchValue.value) params.keyword = searchValue.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 (shots.value.length > 0) {
params.sort = shots.value[0]
if (shots.value[0] == 'tag_group') {
params.sort_id = shots.value[1]
}
}
// 标签参数,所有选中的标签id组成数组
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
if (selectedTagIds.length) {
params.tag_id = selectedTagIds
}
console.log(params)
const { data, current, size, total } = await CrmService.getcustomerList(params)
tableData.value = data.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
gettableList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
gettableList()
}
function statusClass(status: any) {
if (status === 0) return 'status-following'
if (status === 1) return 'status-dealed'
if (status === 2) return 'status-lost'
return ''
}
const router = useRouter()
function goAdd() {
router.push('/crmpc/customer/add')
}
function goEdit(row: any) {
router.push({
path: '/crmpc/customer/show',
query: { id: row.id }
})
}
function goAddFollow(row: any) {
router.push({
path: '/crmpc/customer/followadd',
query: { id: row.id } // 可带客户名等参数
})
}
// 标签选中状态,key为组id,值为选中的标签id
function handleTagClick(groupId: string, tagId: string) {
// if (tagActiveMap[groupId] === tagId) {
// tagActiveMap[groupId] = '' // 取消选中
// } else {
tagActiveMap[groupId] = tagId // 选中
// }
pagination.currentPage = 1
gettableList()
}
//删除
const delectfn = (row: any) => {
ElMessageBox.confirm('确定要移除此客户到公海吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await CrmService.getcustomerdelete({ id: row.id })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
const crmdelectfn = (row: any) => {
ElMessageBox.confirm(
'确定要删除此客户吗?删除后无法恢复,若存在疑问可标记为无效用户或操作移除,将客户移除到公海!',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await CrmService.crmCustomerDelete({ id: row.id, scene: 'force' })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
// 标记无效
const reportfn = (row: any) => {
reportForm.id = row.id
reportForm.desc = ''
reportFileList.value = []
reportDialogVisible.value = true
}
const handleReportUploadSuccess = (_response: any, _file: any, fileList: any[]) => {
reportFileList.value = fileList
}
const handleReportExceed = () => {
ElMessage.warning('最多可上传 9 个附件')
}
const removeReportFile = (_file: any, fileList: any[]) => {
reportFileList.value = fileList
}
const submitReport = async () => {
const attachments = reportFileList.value
.map((f: any) => f?.response?.data?.file_name)
.filter(Boolean)
const res = await CrmService.customerReport({
id: reportForm.id,
remark: reportForm.desc,
img: attachments
})
ElMessage.success(res?.message || '提交成功!')
reportDialogVisible.value = false
gettableList()
}
function processImages(html: string) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
})
return doc.body.innerHTML
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
detailDialogVisible.value = true
}
</script>
<style scoped lang="scss">
.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;
}
}
:deep(.el-badge) {
--el-badge-size: 16px;
}
.itemtag {
margin-top: -16px;
margin-left: -13px;
}
:deep(.el-table__header th) {
background: #f7f9fa !important;
color: #333;
font-weight: bold;
}
:deep(.el-table__empty-block) {
height: 450px !important;
}
:deep(.el-table__body tr.custom-table-row td) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__body tr td:last-child) {
border-right: none !important;
}
:deep(.el-table__body tr:last-child td) {
border-bottom: none !important;
}
:deep(.el-table__header-wrapper th) {
border-bottom: 1px solid #f7f9fa !important;
border-right: 1px solid #f7f9fa !important;
}
:deep(.el-table__header-wrapper th:last-child) {
border-right: none !important;
}
:deep(.el-table) {
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.customer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.customer-search-group {
display: flex;
gap: 12px;
}
.customer-tags {
background: var(--art-main-bg-color);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.filter-label {
width: 120px;
color: #666;
margin-right: 8px;
flex-shrink: 0;
font-size: 14px;
}
.filter-tags {
flex: 1;
color: #333;
font-weight: 500;
font-size: 14px;
}
.customer-tabs {
margin-bottom: 8px;
}
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.customer-table {
background: var(--art-main-bg-color);
border-radius: 8px;
margin-top: 0;
}
.filter-more-btn {
text-align: center;
margin-top: 8px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.tag-items {
cursor: pointer;
border-radius: 4px;
padding: 5px 10px;
transition: background 0.2s;
min-width: 100px;
display: inline-block;
margin-right: 30px;
}
.tag-item {
border-radius: 4px;
padding: 5px 10px;
}
.tag-itemall {
border-radius: 4px;
padding: 5px 10px;
margin-right: 20px;
}
.tag-active {
background: #3471ff;
color: #fff;
}
.tag-active :deep(.el-badge__content) {
color: #3471ff !important; /* 文字颜色 */
background-color: #fff !important; /* 背景色 */
}
</style>
+417 -26
View File
@@ -89,12 +89,50 @@
</div>
<div class="boxshadow">
<!-- Tab切换 -->
<el-tabs v-model="activeTab" class="customer-tabs">
<el-tab-pane label="全部" name=""></el-tab-pane>
<el-tab-pane label="跟进中" name="0"></el-tab-pane>
<el-tab-pane label="已成交" name="1"></el-tab-pane>
<el-tab-pane label="已丢失" name="2"></el-tab-pane>
</el-tabs>
<div class="customer-toolbar">
<el-tabs v-model="activeTab" class="customer-tabs" style="flex: 1">
<el-tab-pane label="全部" name=""></el-tab-pane>
<el-tab-pane label="跟进中" name="0"></el-tab-pane>
<el-tab-pane label="已成交" name="1"></el-tab-pane>
<el-tab-pane label="已丢失" name="2"></el-tab-pane>
</el-tabs>
<div style="width: 240px; display: flex">
<el-form-item label="" class="customer-sort">
<el-cascader :options="options" clearable v-model="shots" placeholder="排序方式" />
</el-form-item>
<el-popover
:visible="columnPopoverVisible"
placement="bottom-end"
:width="260"
trigger="click"
>
<template #reference>
<div
class="btn"
style="margin-left: 20px"
title="自定义列"
@click="columnPopoverVisible = !columnPopoverVisible"
>
<i class="iconfont-sys notice-btn">&#xe72b;</i>
</div>
</template>
<div>
<el-checkbox-group v-model="selectedColumnKeys">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px">
<el-checkbox v-for="col in allColumns" :key="col.key" :label="col.key">
{{ col.label }}
</el-checkbox>
</div>
</el-checkbox-group>
<div style="display: flex; justify-content: end; margin-top: 10px">
<div>
<el-link type="primary" :underline="false" @click="saveCols">保存</el-link>
</div>
</div>
</div>
</el-popover>
</div>
</div>
<!-- 表格 -->
<ArtTable
@@ -110,16 +148,56 @@
lass="customer-table"
>
<template #default>
<el-table-column prop="code" label="编号" align="center" />
<el-table-column prop="contacts" label="客户名称" align="center" />
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
<el-table-column prop="channel_name" label="渠道" align="center" />
<el-table-column prop="country_name" label="国家" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column
v-if="isColVisible('contacts')"
prop="contacts"
label="客户名称"
align="center"
/>
<el-table-column
v-if="isColVisible('magnitude_txt')"
prop="magnitude_txt"
label="客户商机"
align="center"
/>
<el-table-column
v-if="isColVisible('channel_name')"
prop="channel_name"
label="渠道"
align="center"
/>
<el-table-column
v-if="isColVisible('country_name')"
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="describe" label="备注" align="center" />
<!-- <el-table-column prop="email" label="邮箱" align="center" /> -->
<el-table-column
v-if="isColVisible('follow')"
prop="follow"
label="联系方式"
width="240px"
show-overflow-tooltip
>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<el-table-column
v-if="isColVisible('grade_txt')"
prop="grade_txt"
label="等级"
align="center"
/>
<!-- <el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
<span :class="statusClass(scope.row.follow)">
@@ -127,9 +205,40 @@
</span>
</template>
</el-table-column> -->
<el-table-column prop="follow_next" label="下次跟进时间"></el-table-column>
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column
v-if="isColVisible('follow_last')"
prop="follow_last"
label="上次跟进"
></el-table-column>
<el-table-column
v-if="isColVisible('follow_next')"
prop="follow_next"
label="下次跟进"
></el-table-column>
<el-table-column
v-if="isColVisible('enter_user_name')"
prop="enter_user_name"
label="录单人"
></el-table-column>
<el-table-column
v-if="isColVisible('describe')"
prop="describe"
label="备注"
align="center"
/>
<el-table-column
v-if="isColVisible('code')"
prop="code"
label="编号"
align="center"
min-width="120px"
show-overflow-tooltip
/>
<el-table-column
v-if="isColVisible('created_at')"
prop="created_at"
label="创建时间"
></el-table-column>
<el-table-column fixed="right" label="操作" width="200px" align="center">
<template #default="scope">
<el-link
@@ -159,13 +268,27 @@
>
<i class="iconfont-sys">&#xe6bf;</i> 跟进
</el-link>
<el-link type="danger" underline="never" @click="reportfn(scope.row)">
<el-link
type="warning"
underline="never"
@click="reportfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe701;</i> 标记无效
</el-link>
<el-link
type="danger"
underline="never"
@click="delectfn(scope.row)"
style="margin-right: 8px"
v-if="hasAuth('crm_customer@remove')"
>
<i class="iconfont-sys">&#xe893;</i> 移除
</el-link>
<el-link
type="danger"
underline="never"
@click="crmdelectfn(scope.row)"
v-if="hasAuth('crm_customer@delete')"
>
<i class="iconfont-sys">&#xe6d4;</i> 删除
@@ -177,15 +300,94 @@
</div>
</div>
<el-dialog v-model="detailDialogVisible" title="详情">
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
<div class="detail-info-table">
<div class="row">
<div>客户名称:{{ detailRow.contacts }}</div>
<div>电话:{{ detailRow.phone }}</div>
<div>国家:{{ detailRow.country_name }}</div>
</div>
<div class="row">
<div>渠道:{{ detailRow.channel_name }}</div>
<div>邮箱:{{ detailRow.email }}</div>
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
</div>
<div class="row">
<div>发布人:{{ detailRow.enter_user_name }}</div>
<div>跟单人:{{ detailRow.user_name }}</div>
<div>录单时间:{{ detailRow.created_at }}</div>
</div>
<div class="row">
<div>
等级:
<span>
{{ detailRow.magnitude_txt }}
</span>
</div>
<div>
状态:<span>{{ detailRow.grade_txt }}</span>
</div>
<div>
跟进状态:<span>{{ detailRow.follow_txt }}</span>
</div>
</div>
<div class="row">
<div>备注:{{ detailRow.describe }}</div>
</div>
</div>
<div v-if="detailRow.desc_collect != '' && detailRow.desc_collect != 'null'" class="dtitle"
>详情内容</div
>
<div
v-if="detailRow.desc_collect != '' && detailRow.desc_collect != 'null'"
v-html="detailRow.desc_collect"
class="detils-content"
></div>
</el-dialog>
<el-dialog v-model="reportDialogVisible" title="标记无效" width="600px">
<el-form :model="reportForm" label-width="80px">
<el-form-item label="描述">
<el-input
v-model="reportForm.desc"
type="textarea"
:rows="4"
placeholder="请输入无效原因或说明"
/>
</el-form-item>
<el-form-item label="附件">
<el-upload
v-model:file-list="reportFileList"
:headers="uploadHeaders"
:action="reportUploadAction"
multiple
:limit="9"
:on-success="handleReportUploadSuccess"
:on-exceed="handleReportExceed"
:on-remove="removeReportFile"
>
<el-button type="primary">上传附件</el-button>
<!-- <template #tip>
<div class="el-upload__tip">支持图片、视频或文件</div>
</template> -->
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="reportDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitReport">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
defineOptions({ name: 'Customer_index' })
const { hasAuth } = useAuth()
const searchValue = ref('')
const selectValue = ref('')
@@ -195,6 +397,55 @@
const loading = ref(false)
const detailDialogVisible = ref(false)
const detailRow = ref<any>(null)
const reportDialogVisible = ref(false)
const reportForm = reactive({ id: '', desc: '' })
const reportFileList = ref<any[]>([])
const { uploadHeaders } = useUploadHeaders()
const reportUploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload`
const columnPopoverVisible = ref(false) // 控制自定义列popover的显示状态
// 自定义列配置
type ColumnOption = { key: string; label: string }
const COLUMN_STORAGE_KEY = 'crmpc_customer_table_columns'
const allColumns = ref<ColumnOption[]>([
{ key: 'code', label: '编号' },
{ key: 'contacts', label: '客户名称' },
{ key: 'magnitude_txt', label: '客户商机' },
{ key: 'channel_name', label: '渠道' },
{ key: 'country_name', label: '国家' },
{ key: 'follow', label: '联系方式' },
{ key: 'grade_txt', label: '等级' },
{ key: 'follow_last', label: '上次跟进' },
{ key: 'follow_next', label: '下次跟进' },
{ key: 'enter_user_name', label: '录单人' },
{ key: 'describe', label: '备注' },
{ key: 'created_at', label: '创建时间' }
])
const defaultColumnKeys = allColumns.value.map((c) => c.key)
const selectedColumnKeys = ref<string[]>([])
const loadCols = () => {
try {
const saved = localStorage.getItem(COLUMN_STORAGE_KEY)
selectedColumnKeys.value = saved ? JSON.parse(saved) : defaultColumnKeys
} catch (_) {
selectedColumnKeys.value = defaultColumnKeys
}
}
const saveCols = () => {
if (selectedColumnKeys.value.length === 0) {
ElMessage.warning('至少选择一列显示')
return
}
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(selectedColumnKeys.value))
ElMessage.success('保存成功')
columnPopoverVisible.value = false // 隐藏popover
}
// const resetCols = () => {
// selectedColumnKeys.value = [...defaultColumnKeys]
// saveCols()
// }
const isColVisible = (key: string) => selectedColumnKeys.value.includes(key)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
@@ -206,6 +457,19 @@
{ label: '近三天', value: '1' },
{ label: '近七天', value: '2' }
]
const shots = ref([])
const options = ref<
Array<{
label: string
value: string
children: Array<{ label: string; value: string }>
}>
>([
{ label: '默认排序', value: 'id', children: [] },
{ label: '最后跟进', value: 'follow_last', children: [] },
{ label: '下次跟进', value: 'follow_next', children: [] },
{ label: '标签组', value: 'tag_group', children: [] }
])
const countryOptions = ref<any>()
//国家
const getarea = async () => {
@@ -228,6 +492,10 @@
tagActiveMap[group.id] = ''
}
})
options.value[3].children = tagGroups.value.map((group) => ({
label: group.name,
value: group.id
}))
}
// 计算前3组和更多组
// eslint-disable-next-line vue/valid-define-props
@@ -240,9 +508,31 @@
getarea()
gettableList()
tagGrouplistfn()
loadCols()
// 添加全局点击事件监听,点击其他地方时隐藏popover
document.addEventListener('click', handleGlobalClick)
})
onUnmounted(() => {
// 移除全局点击事件监听
document.removeEventListener('click', handleGlobalClick)
})
// 处理全局点击事件
const handleGlobalClick = (event: Event) => {
const target = event.target as HTMLElement
const popover = document.querySelector('.el-popover')
const referenceBtn = document.querySelector('.btn[title="自定义列"]')
// 如果点击的不是popover内部元素,也不是触发按钮,则隐藏popover
if (popover && !popover.contains(target) && referenceBtn && !referenceBtn.contains(target)) {
columnPopoverVisible.value = false
}
}
// tabs 切换联动
watch([activeTab, searchValue, selectValue, country_id], () => {
watch([activeTab, searchValue, selectValue, country_id, shots], () => {
console.log('tabs 联动' + shots.value)
pagination.currentPage = 1
gettableList()
})
@@ -258,6 +548,12 @@
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 (shots.value.length > 0) {
params.sort = shots.value[0]
if (shots.value[0] == 'tag_group') {
params.sort_id = shots.value[1]
}
}
// 标签参数,所有选中的标签id组成数组
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
if (selectedTagIds.length) {
@@ -318,7 +614,7 @@
}
//删除
const delectfn = (row: any) => {
ElMessageBox.confirm('确定要删除此用户吗?', '提示', {
ElMessageBox.confirm('确定要移除此客户到公海吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@@ -333,22 +629,54 @@
// 用户点击了取消
})
}
// 标记无效
const reportfn = (row: any) => {
ElMessageBox.confirm('确定要标记无效吗?', '提示', {
const crmdelectfn = (row: any) => {
ElMessageBox.confirm(
'确定要删除此客户吗?删除后无法恢复,若存在疑问可标记为无效用户或操作移除,将客户移除到公海!',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await CrmService.customerReport({ id: row.id })
const res = await CrmService.crmCustomerDelete({ id: row.id, scene: 'force' })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
// 标记无效
const reportfn = (row: any) => {
reportForm.id = row.id
reportForm.desc = ''
reportFileList.value = []
reportDialogVisible.value = true
}
const handleReportUploadSuccess = (_response: any, _file: any, fileList: any[]) => {
reportFileList.value = fileList
}
const handleReportExceed = () => {
ElMessage.warning('最多可上传 9 个附件')
}
const removeReportFile = (_file: any, fileList: any[]) => {
reportFileList.value = fileList
}
const submitReport = async () => {
const attachments = reportFileList.value
.map((f: any) => f?.response?.data?.file_name)
.filter(Boolean)
const res = await CrmService.customerReport({
id: reportForm.id,
remark: reportForm.desc,
img: attachments
})
ElMessage.success(res?.message || '提交成功!')
reportDialogVisible.value = false
gettableList()
}
function processImages(html: string) {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
@@ -366,6 +694,37 @@
</script>
<style scoped lang="scss">
.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;
}
}
:deep(.el-badge) {
--el-badge-size: 16px;
}
@@ -520,4 +879,36 @@
color: #3471ff !important; /* 文字颜色 */
background-color: #fff !important; /* 背景色 */
}
.btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
margin-left: 10px;
color: var(--art-gray-700);
cursor: pointer;
background-color: rgba(var(--art-gray-200-rgb), 0.8);
border-radius: 6px;
transition: color 0.3s;
transition: all 0.3s;
i {
font-size: 20px;
color: var(--art-gray-700);
user-select: none;
}
&:hover {
background-color: rgba(var(--art-gray-300-rgb), 0.75);
i {
color: var(--art-gray-800);
}
}
}
.customer-sort {
width: 150px;
margin-left: 20px;
}
</style>
+14 -2
View File
@@ -30,9 +30,20 @@
>
<template #default>
<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="email" label="邮箱"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column> -->
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
<template #default="scope">
<div
>WhatsApp:{{
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
}}</div
>
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
</template>
</el-table-column>
<el-table-column prop="magnitude_txt" label="客户商机"></el-table-column>
<el-table-column prop="channel_name" label="渠道"></el-table-column>
<el-table-column prop="country_name" label="国家"></el-table-column>
@@ -68,6 +79,7 @@
import dayjs from 'dayjs'
import { ElMessageBox } from 'element-plus'
import { CrmService } from '@/api/crmApi'
defineOptions({ name: 'CrmHighseas' })
const loading = ref(false)
const pagination = reactive({
currentPage: 1,
+16 -20
View File
@@ -1,21 +1,17 @@
<template>
<div class="console">
<CardList></CardList>
<!-- <AboutProject /> -->
</div>
</template>
<script setup lang="ts">
import CardList from './widget/CardList.vue'
import { useCommon } from '@/composables/useCommon'
defineOptions({ name: 'Console' })
useCommon().scrollToTop()
</script>
<style lang="scss" scoped>
@use './style';
</style>
<div class="console">
<CardList></CardList>
</div>
</template>
<script setup lang="ts">
import CardList from './widget/CardList.vue'
import { useCommon } from '@/composables/useCommon'
defineOptions({ name: 'CrmHome' })
useCommon().scrollToTop()
</script>
<style lang="scss" scoped>
@use './style';
</style>
+1
View File
@@ -38,6 +38,7 @@
import { ref, onMounted } from 'vue'
import { ElMessageBox } from 'element-plus'
import { CrmService } from '@/api/crmApi'
defineOptions({ name: 'RecyclePage' })
const loading = ref(false)
const pagination = reactive({
currentPage: 1,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+471
View File
@@ -0,0 +1,471 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<!-- <div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div> -->
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</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"
>
<template #default>
<ElTableColumn label="备货单编号" prop="order_no" min-width="140" />
<ElTableColumn label="阿里订单号/PI号" prop="pi_no" min-width="160" />
<ElTableColumn label="客户名称" prop="customer_snapshot" min-width="120" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" min-width="120" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" min-width="100" />
<ElTableColumn label="日期" prop="created_at" min-width="120" />
<ElTableColumn label="状态" prop="approval_status">
<template #default="scope">
<div v-if="scope.row.appro_status">
<div
><span :class="statusClass(scope.row.appro_status.status)">{{
scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : ''
}}</span>
<span v-if="scope.row.appro_status.name != ''"
>【{{ scope.row.appro_status.name }}】</span
>
</div>
</div>
<div v-else></div>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="160" align="center">
<template #default="scope">
<el-link
v-if="scope.row.file_url != null && scope.row.file_url != ''"
type="warning"
underline="never"
@click="printfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe782;</i> 打印预览
</el-link>
<el-link
v-if="scope.row.approval_status == 0"
type="primary"
underline="never"
@click="handleEdit(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe6bf;</i> 编辑
</el-link>
<el-link
v-if="scope.row.approval_status == 0"
type="success"
underline="never"
@click="handleapproval(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe641;</i> 提交
</el-link>
<el-link type="primary" underline="never" @click="handleView1(scope.row)">
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 订单详情弹窗(使用公共内容组件) -->
<el-dialog
v-model="orderDialog.visible"
:title="orderDialog.title"
width="95%"
:close-on-click-modal="false"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
<div class="boxshadow">
<div class="form-title">
<!-- <span class="border"></span> -->
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="orderDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="download">下载</el-button>
</div>
<div
v-if="pdf.images.length"
style="overflow: auto; border: 1px solid #eee; border-radius: 6px; padding: 8px"
>
<img
v-for="(img, idx) in pdf.images"
:key="idx"
:src="img"
style="display: block; width: 100%; margin-bottom: 8px; background: #fff"
/>
</div>
<iframe
v-else-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
import { usePdfPreview } from '@/composables/usePdfPreview'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'CrmFinalList' })
// 订单详情弹窗
const orderDialog = reactive({
visible: false,
title: '订单详情',
orderData: {} as OrderData
})
const { hasAuth } = useAuth()
function statusClass(status: any) {
if (status === 'rejected') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 1, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await CrmService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const handleEdit = (row: any) => {
router.push({
path: '/crmpc/stocking/final_edit',
query: { id: row.id } // 可带客户名等参数
})
}
const handleapproval = (row: any) => {
CrmService.stockultimateapproval({ id: row.id })
.then((res) => {
ElMessage.success(res?.message || '修改成功')
getList()
})
.catch((err) => {
console.log(err)
})
}
// PDF 预览/打印(公共)
const { pdf, frameRef, openWithResponse, download, preparePreview, prepareImages } =
usePdfPreview()
//打印
const printfn = (row: any) => {
CrmService.printStock({ id: row.id })
.then((res) => {
openWithResponse(res)
// 优先转成图片以适配不带插件的环境
prepareImages()
// 同时保留 blob 预览作为兜底
preparePreview()
})
.catch((err) => {
console.log(err)
})
}
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const handleView1 = async (row: any) => {
const res = await CrmService.checkStock({ id: row.id })
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
if (res) {
// 获取订单详情成功
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
// router.push({
// path: '/crmpc/stocking/final_show',
// query: { id: row.object_id } // 可带客户名等参数
// })
// router.push('/crmpc/stocking/final_show')
// TODO: 跳转或弹窗查看备货单
}
// const handleView1 = (row: any) => {
// router.push({
// path: '/crmpc/stocking/final_show',
// query: { id: row.id } // 可带客户名等参数
// })
// // TODO: 跳转或弹窗查看备货单
// }
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable { cursor: pointer; }
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
</style>
+701
View File
@@ -0,0 +1,701 @@
<template>
<div class="stocking-show-page">
<div class="header boxshadow">
<div class="header-left">
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
<div class="sub">
<span class="mr40">备货单编号:{{ header.order_no }}</span>
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain @click="printCurrent">打印整单</ElButton>
</div>
<!-- 基础信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="阿里订单号/PI号">
<ElInput v-model="base.pn_no" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="日期">
<ElInput v-model="base.date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElFormItem label="客户来源">
<ElInput v-model="base.channel" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户名称">
<ElInput v-model="base.customer_name" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户所在国">
<ElInput v-model="base.client_country" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 订单详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
<div class="items-wrap">
<div class="item-row" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12">
<ElCol :span="8">
<ElFormItem label="车型" label-width="64px">
<ElInput v-model="item.model" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/外" label-width="72px">
<ElInput v-model="item.color_out" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="内饰" label-width="52px">
<ElInput v-model="item.color_in" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="改装" label-width="52px">
<ElInput v-model="item.modify" disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
<template #append>{{ item.unit_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model="item.total_price" disabled>
<template #append>{{ item.total_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
<template #append>{{ item.helper_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
<template #append>{{ item.port_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="里程" label-width="52px">
<ElInput v-model="item.mileage" disabled />
</ElFormItem>
</ElCol>
<template v-if="(item.mileage ?? 0) <= 0">
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<template v-else>
<ElCol :span="4">
<ElFormItem label="挂牌价" label-width="55px">
<ElInput v-model="item.listing_price" disabled>
<template #append>{{ item.listing_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="二手减价" label-width="72px">
<ElInput v-model="item.reduction_price" disabled>
<template #append>{{ item.reduction_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled :class="Number(item.profit) < 0 ? 'profit-negative' : ''">
<template #append>{{ item.profit_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="车架号" label-width="70px">
<ElInput v-model.number="item.vin" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<ElRow :gutter="12">
<ElCol :span="6">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="6">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="base.contract_rate" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<!-- 运输详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
defineOptions({ name: 'CrmStockingDetail' })
const printCurrent = async () => {
const res = await CrmService.printStock({ id: stocking_id })
openWithResponse(res)
preparePreview()
}
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const ship = reactive({
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
function statusClass(status: any) {
if (status === 'rejected') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const listing = toMoneyView(it?.listing_price)
const reduction = toMoneyView(it?.reduction_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
mileage: it?.mileage ?? 0,
vin: it?.vin || '',
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
listing_price: listing.amount,
listing_price_currency: listing.currency,
reduction_price: reduction.amount,
reduction_price_currency: reduction.currency,
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page { min-height: 100vh; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 { margin-right: 40px; }
}
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
.form-title .title { margin-left: 6px; }
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
/* 利润为负数时,输入文字变红(兼容 Element Plus disabled 样式) */
:deep(.profit-negative .el-input__wrapper) {
color: #ff4d4f !important;
}
:deep(.profit-negative .el-input__inner) {
color: #ff4d4f !important;
-webkit-text-fill-color: #ff4d4f !important; /* 修复 WebKit 在 disabled 下的文本颜色 */
}
:deep(.profit-negative.is-disabled .el-input__wrapper) {
background-color: var(--el-fill-color-blank) !important;
box-shadow: 0 0 0 1px var(--el-input-border-color, #dcdfe6) inset !important;
}
</style>
+688
View File
@@ -0,0 +1,688 @@
<template>
<div class="stocking-show-page">
<div class="header boxshadow">
<div class="header-left">
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
<div class="sub">
<span class="mr40">备货单编号:{{ header.order_no }}</span>
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain @click="printCurrent">打印整单</ElButton>
</div>
<!-- 基础信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="阿里订单号/PI号">
<ElInput v-model="base.pn_no" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="日期">
<ElInput v-model="base.date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElFormItem label="客户来源">
<ElInput v-model="base.channel" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户名称">
<ElInput v-model="base.customer_name" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户所在国">
<ElInput v-model="base.client_country" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 订单详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
<div class="items-wrap">
<div class="item-row" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12">
<ElCol :span="8">
<ElFormItem label="车型" label-width="64px">
<ElInput v-model="item.model" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/外" label-width="72px">
<ElInput v-model="item.color_out" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="内饰" label-width="52px">
<ElInput v-model="item.color_in" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="改装" label-width="52px">
<ElInput v-model="item.modify" disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
<template #append>{{ item.unit_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model="item.total_price" disabled>
<template #append>{{ item.total_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
<template #append>{{ item.helper_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
<template #append>{{ item.port_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="里程" label-width="52px">
<ElInput v-model="item.mileage" disabled />
</ElFormItem>
</ElCol>
<template v-if="(item.mileage ?? 0) <= 0">
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<template v-else>
<ElCol :span="4">
<ElFormItem label="挂牌价" label-width="55px">
<ElInput v-model="item.listing_price" disabled>
<template #append>{{ item.listing_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="二手减价" label-width="72px">
<ElInput v-model="item.reduction_price" disabled>
<template #append>{{ item.reduction_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled :class="Number(item.profit) < 0 ? 'profit-negative' : ''">
<template #append>{{ item.profit_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
</div>
<ElRow :gutter="12">
<ElCol :span="6">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="6">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="base.contract_rate" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<!-- 运输详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
defineOptions({ name: 'CrmStockingShow' })
const printCurrent = async () => {
const res = await CrmService.printStock({ id: stocking_id })
openWithResponse(res)
preparePreview()
}
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const ship = reactive({
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
mileage: it?.mileage ?? 0,
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
listing_price: it?.listing_price?.amount ?? '',
listing_price_currency: it?.listing_price?.currency ?? '',
reduction_price: it?.reduction_price?.amount ?? '',
reduction_price_currency: it?.reduction_price?.currency ?? '',
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page { min-height: 100vh; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 { margin-right: 40px; }
}
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
.form-title .title { margin-left: 6px; }
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
/* 利润为负数时,输入文字变红(兼容 Element Plus disabled 样式) */
:deep(.profit-negative .el-input__wrapper) {
color: #ff4d4f !important;
}
:deep(.profit-negative .el-input__inner) {
color: #ff4d4f !important;
-webkit-text-fill-color: #ff4d4f !important; /* 修复 WebKit 在 disabled 下的文本颜色 */
}
:deep(.profit-negative.is-disabled .el-input__wrapper) {
background-color: var(--el-fill-color-blank) !important;
box-shadow: 0 0 0 1px var(--el-input-border-color, #dcdfe6) inset !important;
}
</style>
File diff suppressed because it is too large Load Diff
+553
View File
@@ -0,0 +1,553 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</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"
>
<template #default>
<ElTableColumn label="备货单编号" prop="order_no" />
<ElTableColumn label="阿里订单号/PI号" prop="pi_no" />
<ElTableColumn label="客户名称" prop="customer_snapshot" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" />
<ElTableColumn label="日期" prop="created_at" />
<!-- <ElTableColumn label="补充" prop="remark" /> -->
<ElTableColumn label="状态" prop="approval_status">
<template #default="scope">
<div v-if="scope.row.appro_status">
<div
><span :class="statusClass(scope.row.appro_status.status)">{{
scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : ''
}}</span>
<span v-if="scope.row.appro_status.name != ''"
>【{{ scope.row.appro_status.name }}】</span
>
</div>
</div>
<div v-else></div>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="120" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
v-if="scope.row.file_url != null && scope.row.file_url != ''"
type="warning"
underline="never"
@click="printfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe782;</i> 打印预览
</el-link>
<el-link
v-if="scope.row.approval_status == 0"
type="success"
underline="never"
@click="handleapproval(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe641;</i> 提交
</el-link>
<el-link
v-if="scope.row.approval_status == 0"
type="primary"
underline="never"
@click="handleEdit(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe6bf;</i> 编辑
</el-link>
<el-link
type="success"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看
</el-link>
<el-link
v-if="
scope.row.is_use == 0 &&
scope.row.approval_status == 1 &&(scope.row.appro_status?scope.row.appro_status.status=='approved':false)
"
type="primary"
underline="never"
@click="handleCreateRepair(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 创建终版
</el-link>
<el-link
v-if="scope.row.is_use == 1"
type="success"
underline="never"
@click="handleView1(scope.row)"
>
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
<el-link type="warning" underline="never" @click="copys(scope.row)">
<i class="iconfont-sys">&#xe8b7;</i> 复制
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="download">下载</el-button>
</div>
<div
v-if="pdf.images.length"
style="overflow: auto; border: 1px solid #eee; border-radius: 6px; padding: 8px"
>
<img
v-for="(img, idx) in pdf.images"
:key="idx"
:src="img"
style="display: block; width: 100%; margin-bottom: 8px; background: #fff"
/>
</div>
<iframe
v-else-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
/>
</div>
</el-dialog>
<!-- 订单详情弹窗(使用公共内容组件) -->
<el-dialog
v-model="orderDialog.visible"
:title="orderDialog.title"
width="95%"
:close-on-click-modal="false"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
<div class="boxshadow">
<div class="form-title">
<!-- <span class="border"></span> -->
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="orderDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 终版详情 -->
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
import { CrmService } from '@/api/crmApi'
import { usePdfPreview } from '@/composables/usePdfPreview'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
// import { useAuth } from '@/composables/useAuth'
// const { hasAuth } = useAuth()
defineOptions({ name: 'CrmStockingList' }) // 定义组件名称,用于 KeepAlive 缓存控制
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// PDF 预览/打印(公共)
const { pdf, frameRef, openWithResponse, download, preparePreview, prepareImages } =
usePdfPreview()
// 订单详情弹窗
const orderDialog = reactive({
visible: false,
title: '订单详情',
orderData: {} as OrderData
})
onMounted(() => {
getList()
})
function statusClass(status: any) {
if (status === 'rejected') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await CrmService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const handleCreate = () => {
// TODO: 跳转到创建备货单页面
router.push('/crmpc/stocking/Add')
}
const handleView = async (row: any) => {
try {
// 获取订单详情数据
const res = await CrmService.checkStock({ id: row.id })
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
if (res) {
// 转换数据格式以适配 OrderDetailDialog
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
} catch (error) {
console.error('获取订单详情失败:', error)
ElMessage.error('获取订单详情失败')
}
}
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const handleView1 = async (row: any) => {
const res = await CrmService.checkStock({ id: row.object_id })
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
if (res) {
// 获取订单详情成功
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
// router.push({
// path: '/crmpc/stocking/final_show',
// query: { id: row.object_id } // 可带客户名等参数
// })
// router.push('/crmpc/stocking/final_show')
// TODO: 跳转或弹窗查看备货单
}
const handleCreateRepair = (row: any) => {
router.push({
path: '/crmpc/stocking/final_add',
query: { id: row.id } // 可带客户名等参数
})
// TODO: 跳转创建维修保单
}
const handleEdit = (row: any) => {
router.push({
path: '/crmpc/stocking/stocking_edit',
query: { id: row.id } // 可带客户名等参数
})
}
const handleapproval = (row: any) => {
CrmService.submitStock({ id: row.id })
.then((res) => {
ElMessage.success(res?.message || '修改成功')
getList()
})
.catch((err) => {
console.log(err)
})
}
//打印
const printfn = (row: any) => {
CrmService.printStock({ id: row.id })
.then((res) => {
openWithResponse(res)
// 优先转成图片以适配不带插件的环境
prepareImages()
// 同时保留 blob 预览作为兜底
preparePreview()
})
.catch((err) => {
console.log(err)
})
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
// 订单详情打印
const handleOrderPrint = (data: OrderData) => {
console.log('打印订单详情:', data)
// 可以在这里添加具体的打印逻辑
window.print()
}
//复制订单
const copys = (row: any) => {
console.log('复制订单:', row.id)
router.push({
path: '/crmpc/stocking/Add',
query: { id: row.id } // 可带客户名等参数
})
// 添加具体的复制逻辑
}
</script>
<style scoped lang="scss">
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
.status-following {
color: #00a43f;
font-weight: bold;
}
.status-dealed {
color: #3471ff;
font-weight: bold;
}
.status-lost {
color: #ff0000;
font-weight: bold;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
</style>
+27 -13
View File
@@ -41,16 +41,29 @@
>
<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="客户名称"></el-table-column>
<el-table-column prop="phone" label="电话"></el-table-column>
<el-table-column prop="contacts" label="客户名称" align="center"></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="email" label="邮箱"></el-table-column>
<el-table-column prop="magnitude_txt" label="客户商机"></el-table-column>
<el-table-column prop="channel_name" label="渠道"></el-table-column>
<el-table-column prop="country_name" label="国家"></el-table-column>
<el-table-column prop="user_name" label="跟单人"></el-table-column>
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column> -->
<el-table-column prop="follow" label="联系方式" min-width="160px">
<template #default="scope">
<div>WhatsApp:{{ scope.row.WhatsApp == '' ? ' --' : scope.row.WhatsApp }}</div>
<div>电话:{{ scope.row.phone == '' ? ' --' : scope.row.phone }}</div>
<div>邮箱:{{ scope.row.email == '' ? ' --' : scope.row.email }}</div>
</template>
</el-table-column>
<el-table-column prop="magnitude_txt" label="客户商机" align="center"></el-table-column>
<el-table-column prop="channel_name" label="渠道" align="center"></el-table-column>
<el-table-column prop="country_name" label="国家" align="center"></el-table-column>
<el-table-column prop="user_name" label="跟单人" align="center"></el-table-column>
<el-table-column
prop="code"
label="编号"
align="center"
min-width="120px"
show-overflow-tooltip
/>
<el-table-column prop="created_at" label="创建时间" align="center"></el-table-column>
</template>
</ArtTable>
</div>
@@ -87,6 +100,7 @@
import { manageService } from '@/api/adminApi'
const selectValue = ref<string[]>([])
const keyword = ref<string>('')
defineOptions({ name: 'TeamCustomer' })
const { hasAuth } = useAuth()
const loading = ref(false)
const pagination = reactive({
@@ -268,10 +282,10 @@
border: 1px solid #f7f9fa !important;
border-radius: 6px;
}
:deep(.el-table th),
:deep(.el-table td) {
text-align: center !important;
}
// :deep(.el-table th),
// :deep(.el-table td) {
// text-align: center !important;
// }
}
.countdown-danger {
color: #ff0000 !important;
+3
View File
@@ -86,6 +86,9 @@
import { ElImageViewer } from 'element-plus'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
defineOptions({
name: 'CustomerPage'
})
const { hasAuth } = useAuth()
const selectValue = ref<string[]>([])
const keyword = ref<string>('')
+356
View File
@@ -0,0 +1,356 @@
<template>
<div class="color-page">
<div class="color-toolbar boxshadow">
<div class="color-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入颜色编码或名称"
clearable
@keyup.enter="getColorList"
/>
</el-form-item>
<el-form-item label="类型:" label-width="50px">
<el-select v-model="typeFilter" placeholder="全部类型" clearable style="min-width: 120px">
<el-option label="外观颜色" :value="1" />
<el-option label="内饰颜色" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态:" label-width="50px">
<el-select
v-model="statusFilter"
placeholder="全部状态"
clearable
style="min-width: 120px"
>
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getColorList" v-ripple>搜索</el-button>
<el-button @click="resetSearch" v-ripple>重置</el-button>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('color@add')" type="primary">
新增颜色
</ElButton>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="颜色编码" prop="color_code" min-width="120" />
<ElTableColumn label="颜色名称" prop="color_text" min-width="150" />
<ElTableColumn label="类型" prop="type" width="120">
<template #default="scope">
<ElTag :type="scope.row.type === 1 ? 'primary' : 'success'">
{{ scope.row.type === 1 ? '外观颜色' : '内饰颜色' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="排序" prop="sort" width="80" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">
{{ scope.row.status ? '启用' : '停用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('color@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteColor(scope.row)"
v-if="hasAuth('color@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增颜色' : '编辑颜色'"
align-center
width="600px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<ElFormItem label="颜色编码" prop="color_code" v-if="dialogType === 'edit'">
<ElInput v-model="form.color_code" disabled placeholder="请输入颜色编码" />
</ElFormItem>
<ElFormItem label="颜色名称" prop="color_text">
<ElInput v-model="form.color_text" placeholder="请输入颜色名称" />
</ElFormItem>
<ElFormItem label="类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio :label="1">外观颜色</el-radio>
<el-radio :label="2">内饰颜色</el-radio>
</el-radio-group>
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { vehicleService } from '@/api/crmpc/vehicleApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'CrmVehicleColor' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const typeFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
// 表单数据
const form = reactive<any>({
id: '',
color_code: '',
color_text: '',
type: 1,
status: 1,
sort: 0
})
// 表单验证规则
const rules = reactive<FormRules>({
color_text: [{ required: true, message: '请输入颜色名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择颜色类型', trigger: 'change' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getColorList()
})
// 监听搜索条件变化
watch([keyword, typeFilter, statusFilter], () => {
pagination.currentPage = 1
getColorList()
})
// 获取颜色列表
const getColorList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await vehicleService.getColorList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取颜色列表失败:', error)
ElMessage.error('获取颜色列表失败')
} finally {
loading.value = false
}
}
// 重置搜索条件
const resetSearch = () => {
keyword.value = ''
typeFilter.value = undefined
statusFilter.value = undefined
pagination.currentPage = 1
getColorList()
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getColorList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getColorList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式
Object.assign(form, {
id: row.id,
color_code: row.color_code,
color_text: row.color_text,
type: row.type,
status: row.status,
sort: row.sort || 0
})
} else {
// 新增模式
Object.assign(form, {
id: '',
color_code: '',
color_text: '',
type: 1,
status: 1,
sort: 0
})
}
}
// 删除颜色
const deleteColor = (row: any) => {
ElMessageBox.confirm('确定删除该颜色吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await vehicleService.colorDelete({ id: row.id })
ElMessage.success(message)
getColorList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
// 新增
const { message } = await vehicleService.colorAdd({
color_text: form.color_text,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
} else {
// 编辑
const { message } = await vehicleService.colorEdit({
id: form.id,
color_text: form.color_text,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
}
dialogVisible.value = false
getColorList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.color-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.color-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.color-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+361
View File
@@ -0,0 +1,361 @@
<template>
<div class="modification-page">
<div class="modification-toolbar boxshadow">
<div class="modification-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getModificationList"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getModificationList" v-ripple>搜索</el-button>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton
@click="showDialog('add')"
v-ripple
v-if="hasAuth('modification@add')"
type="primary"
>
新增改装项目
</ElButton>
</ElCol>
</ElRow>
<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"
>
<template #default>
<ElTableColumn label="项目编码" prop="code" min-width="120" />
<ElTableColumn label="项目名称" prop="name" min-width="150" />
<ElTableColumn label="改装类型" prop="type" width="120">
<template #default="scope">
<ElTag :type="getTypeTagType(scope.row.type)">
{{ getTypeLabel(scope.row.type) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="排序" prop="sort" width="80" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">
{{ scope.row.status ? '启用' : '停用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('modification@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteModification(scope.row)"
v-if="hasAuth('modification@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增改装项目' : '编辑改装项目'"
align-center
width="600px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<ElFormItem label="项目编码" prop="code" v-if="dialogType === 'edit'">
<ElInput v-model="form.code" disabled placeholder="请输入改装项目编码" />
</ElFormItem>
<ElFormItem label="项目名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入改装项目名称" />
</ElFormItem>
<ElFormItem label="改装类型" prop="type">
<el-select v-model="form.type" placeholder="请选择改装类型" style="width: 100%">
<el-option label="外观改装" :value="1" />
<el-option label="性能改装" :value="2" />
<el-option label="内饰改装" :value="3" />
<el-option label="电子设备" :value="4" />
<el-option label="其他" :value="5" />
</el-select>
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" :max="999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { vehicleService } from '@/api/crmpc/vehicleApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'CrmVehicleModification' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const typeFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
// 表单数据
const form = reactive<any>({
id: '',
code: '',
name: '',
type: 1,
status: 1,
sort: 0
})
// 表单验证规则
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入改装项目名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择改装类型', trigger: 'change' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getModificationList()
})
// 监听搜索条件变化
watch([keyword, typeFilter, statusFilter], () => {
pagination.currentPage = 1
getModificationList()
})
// 获取改装项目列表
const getModificationList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await vehicleService.getModificationList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取改装项目列表失败:', error)
ElMessage.error('获取改装项目列表失败')
} finally {
loading.value = false
}
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getModificationList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getModificationList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式
Object.assign(form, {
id: row.id,
code: row.code,
name: row.name,
type: row.type,
status: row.status,
sort: row.sort || 0
})
} else {
// 新增模式
Object.assign(form, {
id: '',
code: '',
name: '',
type: 1,
status: 1,
sort: 0
})
}
}
// 删除改装项目
const deleteModification = (row: any) => {
ElMessageBox.confirm('确定删除该改装项目吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await vehicleService.modificationDelete({ id: row.id })
ElMessage.success(message)
getModificationList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
// 新增
const { message } = await vehicleService.modificationAdd({
name: form.name,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
} else {
// 编辑
const { message } = await vehicleService.modificationEdit({
id: form.id,
name: form.name,
type: form.type,
status: form.status,
sort: form.sort
})
ElMessage.success(message)
}
dialogVisible.value = false
getModificationList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
// 获取类型标签样式
const getTypeTagType = (type: number): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
const typeMap: Record<number, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
1: 'primary', // 外观改装
2: 'success', // 性能改装
3: 'warning', // 内饰改装
4: 'info', // 电子设备
5: 'danger' // 其他
}
return typeMap[type] || 'info'
}
// 获取类型标签文本
const getTypeLabel = (type: number) => {
const typeMap: Record<number, string> = {
1: '外观改装',
2: '性能改装',
3: '内饰改装',
4: '电子设备',
5: '其他'
}
return typeMap[type] || '未知类型'
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.modification-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.modification-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.modification-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
File diff suppressed because it is too large Load Diff
+74 -64
View File
@@ -19,35 +19,35 @@
<!-- 总体情况 -->
<div class="flex-d">
<!-- <transition name="slide-left"> -->
<div class="summary-section" v-show="showallChart">
<h3 class="section-title" style="margin-bottom: 10px">总体情况</h3>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.totals }}</div>
<div class="summary-label">总单数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.channelCount }}</div>
<div class="summary-label">渠道总数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.todayCount }}</div>
<div class="summary-label">今日录单</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.releaseCount }}</div>
<div class="summary-label">发布人总数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.countryCount }}</div>
<div class="summary-label">国家总数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.trackCount }}</div>
<div class="summary-label">跟单人总数</div>
</div>
<div class="summary-section" v-show="showallChart">
<h3 class="section-title" style="margin-bottom: 10px">总体情况</h3>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.totals }}</div>
<div class="summary-label">总单数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.channelCount }}</div>
<div class="summary-label">渠道总数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.todayCount }}</div>
<div class="summary-label">今日录单</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.releaseCount }}</div>
<div class="summary-label">发布人总数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.countryCount }}</div>
<div class="summary-label">国家总数</div>
</div>
<div class="summary-item">
<div class="summary-number">{{ statisticswholedata.trackCount }}</div>
<div class="summary-label">跟单人总数</div>
</div>
</div>
</div>
<!-- </transition> -->
<div class="icons1" v-if="!showallChart" @click="showallChart = !showallChart" >
<img src="@/assets/img/admin/Vector1.png" alt="" />
@@ -60,23 +60,23 @@
<!-- 发布人计数 -->
<div class="flex-d">
<!-- <transition name="slide-left"> -->
<div class="chart-section" v-show="showPublisherChart">
<div class="chart-header">
<h3 class="section-title">询单计数</h3>
<div class="chart-controls">
<ElSelect
v-model="publisherFilter"
class="filter-select"
@change="updatePublisherChart"
>
<ElOption label="全部" value="0" />
<ElOption label="合塑" value="1" />
<ElOption label="派卡" value="3" />
</ElSelect>
</div>
<div class="chart-section" v-show="showPublisherChart">
<div class="chart-header">
<h3 class="section-title">询单计数</h3>
<div class="chart-controls">
<ElSelect
v-model="publisherFilter"
class="filter-select"
@change="updatePublisherChart"
>
<ElOption label="全部" value="0" />
<ElOption label="合塑" value="1" />
<ElOption label="派卡" value="3" />
</ElSelect>
</div>
<div ref="publisherChartRef" class="chart-container"></div>
</div>
<div ref="publisherChartRef" class="chart-container"></div>
</div>
<!-- </transition> -->
<div class="icons1" v-if="!showPublisherChart" @click="togglePublisherChart" >
<img src="@/assets/img/admin/Vector1.png" alt="" />
@@ -98,34 +98,32 @@
<!-- 各渠道计数 -->
<div class="flex-d-r">
<!-- <transition name="slide-right"> -->
<div class="icons" v-if="showchannelChart" @click="toggleshowchannelChart">
<img src="@/assets/img/admin/Vector1.png" alt=""/>
</div>
<div class="chart-section" v-show="showchannelChart">
<h3 class="section-title" style="margin-bottom: 10px">各渠道计数</h3>
<div ref="channelChartRef" class="chart-container"></div>
</div>
<div class="icons1" v-if="!showchannelChart" @click="toggleshowchannelChart">
<img src="@/assets/img/admin/Vector.png" alt="" />
</div>
<div class="icons" v-if="showchannelChart" @click="toggleshowchannelChart">
<img src="@/assets/img/admin/Vector1.png" alt=""/>
</div>
<div class="chart-section" v-show="showchannelChart">
<h3 class="section-title" style="margin-bottom: 10px">各渠道计数</h3>
<div ref="channelChartRef" class="chart-container"></div>
</div>
<div class="icons1" v-if="!showchannelChart" @click="toggleshowchannelChart">
<img src="@/assets/img/admin/Vector.png" alt="" />
</div>
<!-- </transition> -->
</div>
<!-- 每日录单 -->
<div class="flex-d-r">
<div class="icons" v-if="showOrdersChart" @click="toggleOrdersChart">
<img src="@/assets/img/admin/Vector1.png" alt=""/>
</div>
<img src="@/assets/img/admin/Vector1.png" alt=""/>
</div>
<!-- <transition name="slide-right"> -->
<div class="chart-section" v-show="showOrdersChart">
<h3 class="section-title" style="line-height: 36px">每日录单</h3>
<div ref="dailyOrdersChartRef" class="chart-container"></div>
</div>
<div class="chart-section" v-show="showOrdersChart">
<h3 class="section-title" style="line-height: 36px">每日录单</h3>
<div ref="dailyOrdersChartRef" class="chart-container"></div>
</div>
<!-- </transition> -->
<div class="icons1" v-if="!showOrdersChart" @click="toggleOrdersChart">
<img src="@/assets/img/admin/Vector.png" alt="" />
</div>
<img src="@/assets/img/admin/Vector.png" alt="" />
</div>
</div>
</div>
<!-- 跟单人计数图表区域 -->
@@ -798,7 +796,7 @@
: parseInt(params.data.num))
)
}
},
}
},
series: [
{
@@ -810,9 +808,21 @@
color: '#5D87FF',
shadowBlur: 15,
shadowColor: '#5D87FF',
borderColor: '#fff',
borderColor: '#5D87FF',
borderWidth: 2
},
label: {
show: true, // 显示标签
formatter: (params: any) => params.data.num, // 显示数据值
position: 'inside', // 标签位置(可选:'top', 'left', 'right', 'bottom', 'inside' 等)
color: '#fff', // 标签文字颜色
fontSize: 10, // 标签字体大小
fontWeight: 'bold', // 标签字体粗细
backgroundColor: '#5D87FF', // 背景色
borderColor: '#fff',
borderRadius: 9, // 圆角
padding: [4, 4] // 内边距
},
tooltip: {
show: true,
formatter: function (params: any) {
+164 -92
View File
@@ -111,6 +111,48 @@
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="form.email" :disabled="!isEdit" />
</ElFormItem>
<ElFormItem label="签名" prop="signature" class="right-input">
<template v-if="form.sign_img">
<img :src="sign_img_url" class="signature-img" />
<el-button
v-if="isEdit"
style="margin-left: 12px"
@click="showReSignQR = !showReSignQR"
>
重新签名
</el-button>
<div
v-if="isEdit && showReSignQR"
class="signature-qrcode"
style="margin-top: 20px; margin-left: 20px"
>
<qrcode-vue
:value="qrcodeUrl"
:size="200"
level="M"
render-as="svg"
:include-margin="true"
background="#ffffff"
foreground="#000000"
/>
<div class="qrcode-tip">手机扫码重新签名</div>
</div>
</template>
<template v-else>
<div class="signature-qrcode">
<qrcode-vue
:value="qrcodeUrl"
:size="200"
level="M"
render-as="svg"
:include-margin="true"
background="#ffffff"
foreground="#000000"
/>
<div class="qrcode-tip">手机扫码进行签名</div>
</div>
</template>
</ElFormItem>
</ElRow>
<ElRow>
<ElFormItem
@@ -210,6 +252,8 @@
import { ElForm, FormInstance } from 'element-plus'
import imageUrl from '@/assets/img/crm/upimg.png'
import { CrmService } from '@/api/crmApi'
import { useUserStore } from '@/store/modules/user'
import QrcodeVue from 'qrcode.vue'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
@@ -221,110 +265,135 @@
const imageUrls = ref(imageUrl)
const imageUrlavatar = ref('')
const socialize_pic_full = ref('')
const sign_img_url = ref('')
const countryOptions = ref<any>()
const form = ref<CustomerInfo>({})
//获取公司 getCompanyList
const companyList = ref<any>()
// const companyList = ref<any>()
//国家
const getarea = async () => {
try {
// 这里调用获取品牌列表的接口
const res = await CrmService.getCountry()
countryOptions.value = res||[]
} catch (error) { countryOptions.value = [] }
countryOptions.value = res || []
} catch {
countryOptions.value = []
}
}
const userInfo = ref<CustomerInfo>({})
const getuserInfo = async () => {
const data = await CrmService.userget()
userInfo.value = data
form.value = data
imageUrlavatar.value = data.avatar_full
socialize_pic_full.value = data.socialize_pic_full
const getuserInfo = async () => {
const data = await CrmService.userget()
userInfo.value = data
form.value = data
imageUrlavatar.value = data.avatar_full
socialize_pic_full.value = data.socialize_pic_full
sign_img_url.value = data.sign_img_url
}
const isEdit = ref(false)
const showReSignQR = ref(false)
const isEditPwd = ref(false)
const pwdForm = reactive({
password: '',
password_new: '',
confirmPassword: ''
})
const ruleFormRef = ref<FormInstance>()
// const rules = reactive<FormRules>({
// realName: [
// { required: true, message: '请输入昵称', trigger: 'blur' },
// { min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
// ],
// nikeName: [
// { required: true, message: '请输入昵称', trigger: 'blur' },
// { min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
// ],
// email: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
// mobile: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
// address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
// sex: [{ type: 'array', required: true, message: '请选择性别', trigger: 'blur' }]
// })
const options = [
{
value: '',
label: '未知'
},
{
value: '1',
label: '男'
},
{
value: '2',
label: '女'
}
const isEdit = ref(false)
const isEditPwd = ref(false)
const pwdForm = reactive({
password: '',
password_new: '',
confirmPassword: ''
})
]
onMounted(async () => {
await getarea()
await getuserInfo()
})
const edit = async () => {
if (isEdit.value) {
const payload: any = { ...(form.value as any) }
delete payload.sign_img_url
const res = await CrmService.crmuseredit(payload)
ElMessage.success(res?.message || '操作成功')
getuserInfo()
}
isEdit.value = !isEdit.value
if (!isEdit.value) {
showReSignQR.value = false
}
}
const ruleFormRef = ref<FormInstance>()
// const rules = reactive<FormRules>({
// realName: [
// { required: true, message: '请输入昵称', trigger: 'blur' },
// { min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
// ],
// nikeName: [
// { required: true, message: '请输入昵称', trigger: 'blur' },
// { min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
// ],
// email: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
// mobile: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
// address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
// sex: [{ type: 'array', required: true, message: '请选择性别', trigger: 'blur' }]
// })
const options = [
{
value: '',
label: '未知'
},
{
value: '1',
label: '男'
},
{
value: '2',
label: '女'
const editPwd = async () => {
if (isEditPwd.value) {
// 判断新密码和确认密码是否一致
if (pwdForm.password_new !== pwdForm.confirmPassword) {
ElMessage.error('新密码和确认密码不一致')
return
}
]
onMounted(async () => {
await getarea()
await getuserInfo()
})
const edit = async() => {
if(isEdit.value){
const res = await CrmService.crmuseredit(form.value)
ElMessage.success(res?.message || '操作成功')
getuserInfo()
}
isEdit.value = !isEdit.value
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) || ''
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) || ''
}
// 如需同步到 form.picture
form.value.socialize_pic = response.url || (response.data && response.data.file_name) || ''
}
const qrcodeUrl = computed(() => {
const userStore = useUserStore() as any
const userId = (form.value as any)?.id || (userInfo.value as any)?.id || ''
const base = location.origin
const params = new URLSearchParams({ userId: String(userId) })
// 只保留必要的参数,减少URL长度
if (userStore?.accessToken) params.append('token', userStore.accessToken)
if (userStore?.companyabbr) params.append('corp', userStore.companyabbr)
if (userStore?.companyid) params.append('corpid', userStore.companyid)
if (userStore?.systemsabbr) params.append('sys', userStore.systemsabbr)
if (userStore?.systemsid) params.append('sysid', userStore.systemsid)
if (userStore?.loginType) params.append('type', userStore.loginType)
if (userStore?.language) params.append('lang', userStore.language)
return `${base}/auth/signature/mobile?${params.toString()}`
})
</script>
<style lang="scss">
@@ -340,7 +409,7 @@
</style>
<style lang="scss" scoped>
:deep(.qrcode-radio-group .el-form-item__content){
:deep(.qrcode-radio-group .el-form-item__content) {
height: 40px;
}
.avatar {
@@ -349,7 +418,7 @@
border-radius: 50%;
object-fit: cover;
}
.qrcode-img{
.qrcode-img {
width: 80px;
height: 80px;
}
@@ -384,7 +453,7 @@
overflow: hidden;
text-align: center;
// background: var(--art-main-bg-color);
background: linear-gradient(180deg, #E9F0FF 0%, #FFFFFF 67.9%);
background: linear-gradient(180deg, #e9f0ff 0%, #ffffff 67.9%);
border-radius: $box-radius;
.avatar {
@@ -542,4 +611,7 @@
}
}
}
.signature-img {
width: 160px;
}
</style>
+301 -305
View File
@@ -1,318 +1,313 @@
<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 v-if="hasAuth('work_order_self@add')" @click="showDialog('add')" v-ripple>
新增
</ElButton>
</ElCol>
</ElRow>
<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 v-if="hasAuth('work_order_self@add')" @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"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="工单编号" prop="code" />
<ElTableColumn label="问题分类" prop="categorie_id">
<template #default="scope">
<div v-html="scope.row.categorie ? scope.row.categorie.name : ''"></div>
</template>
</ElTableColumn>
<ElTableColumn label="问题描述" prop="content">
<template #default="scope">
<div class="multi-line-ellipsis" v-html="scope.row.content"></div>
</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status == 1 ? 'info' : scope.row.status == 2 ?'primary' :scope.row.status == 4 ?'success' : 'danger'">
{{ scope.row.status == 1 ? '待受理' : scope.row.status == 2 ? '处理中' :
scope.row.status == 4 ? "已完成":'关闭' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" width="240px">
<template #default="scope">
<ElRow>
<el-button
v-if="scope.row.status == 1 && hasAuth('work_order_self@edit')"
link
@click="showDialog('edit', scope.row)"
>
编辑
</el-button>
<el-button
v-if="hasAuth('work_order_self@status')"
link
@click="statusEdit(scope.row)"
>
状态编辑
</el-button>
<el-button
v-if="hasAuth('work_order_self@conversation_list')"
link
@click="openChat(scope.row)"
>
查看/回复
</el-button>
<!-- <el-button link @click="deleteRole()"> 删除 </el-button> -->
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
width="60%"
align-center
>
<ElForm ref="formRef" :model="form" label-width="120px" style="padding-right: 30px">
<ElFormItem label="问题分类" prop="categorie_id">
<ElSelect v-model="form.categorie_id">
<ElOption v-for="type in categorieSelectList" :key="type.id" :value="type.id" :label="type.name" />
</ElSelect>
</ElFormItem>
<ElFormItem label="问题描述" prop="content">
<!-- <ElInput v-model="form.des" type="textarea" :rows="3" /> -->
<!-- 富文本编辑器 -->
<ArtWangEditor class="el-top" v-model="editorHtml" />
</ElFormItem>
<ElFormItem label="上传附件" prop="attachment">
<ElUpload
v-model:file-list="fileList"
class="upload-demo"
:action="actionImageUplode"
:headers="uploadHeaders"
multiple
:limit="3"
:on-exceed="handleExceed"
>
<el-button type="primary" :disabled="fileList.length >= 3">上传附件</el-button>
</ElUpload>
<!-- <ElUpload
v-model:file-list="fileList"
class="upload-demo"
:action="actionImageUplode"
:on-success="handleUploadSuccess"
:headers="uploadHeaders"
multiple
:limit="3"
>
<el-button type="primary">上传附件</el-button>
</ElUpload> -->
</ElFormItem>
<ElFormItem label="" prop="">
<!-- 附件预览区 -->
<div class="attachment-preview">
<div v-if="fileList.length" class="attachment-list">
<div v-for="(file, idx) in fileList" :key="file.name" class="attachment-item">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
<img
:src="file.file_url ? file.file_url : file.response.data.file_url"
:alt="file.name"
class="attachment-thumb"
@click="
handleImageClick(
imageList.findIndex((url) =>
url === file.file_url ? file.file_url : file.response.data.file_url
)
)
"
/>
</template>
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(file.name)">
<video
:src="file.file_url ? file.file_url : file.response.data.file_url"
controls
class="attachment-thumb"
@click.prevent="
handleVideoClick(file.file_url ? file.file_url : file.response.data.file_url)
"
></video>
</template>
<template v-else>
<a
:href="file.file_url ? file.file_url : file.response.data.file_url"
target="_blank"
>{{ file.name }}</a
>
</template>
<div class="attachment-filename">{{ file.name }}</div>
</div>
</div>
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<el-drawer v-model="isDrawerVisible" :size="isMobile ? '100%' : '480px'" :with-header="false">
<div class="chat-container">
<div class="header">
<div class="header-left">
<span class="name">工单记录</span>
</div>
<div class="header-right">
<el-icon class="icon-close" :size="20" @click="closeChat">
<Close />
</el-icon>
</div>
</div>
<!-- 聊天消息区域 -->
<div class="chat-messages" ref="messageContainer">
<template v-for="(message, index) in messages" :key="index">
<div :class="['message-item', String(message.user_id) === String(userid) ? 'message-right' : 'message-left']">
<el-avatar :size="32" :src="message.user_avatar" class="message-avatar" />
<div class="message-content">
<div class="message-info">
<span class="sender-name">{{ message.user_name }}</span>
<span class="message-time">{{ message.updated_at }}</span>
</div>
<div class="message-text">{{ message.content }}</div>
<!-- 附件显示区域 -->
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
<img
:src="attachment.storage_path_full"
:alt="attachment.original_name"
class="attachment-thumb"
@click="handleChatImageClick(attachment.storage_path_full)"
/>
</template>
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
<video
:src="attachment.storage_path_full"
controls
class="attachment-thumb"
@click.prevent="handleChatVideoClick(attachment.storage_path_full)"
></video>
</template>
<template v-else>
<div class="file-attachment">
<el-icon class="file-icon"><Document /></el-icon>
<span class="file-name">{{ attachment.original_name }}</span>
<el-button
type="primary"
size="small"
link
@click="downloadAttachment(attachment)"
>
下载
</el-button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
<div class="chat-input" v-if="hasAuth('work_order_self@conversation_add')">
<!-- 附件上传区域 -->
<div class="chat-attachment-upload">
<el-upload
v-model:file-list="chatFileList"
class="chat-upload"
:action="actionImageUplode"
:on-success="handleChatUploadSuccess"
:headers="uploadHeaders"
multiple
:limit="5"
:on-exceed="handleChatExceed"
:show-file-list="false"
<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"
>
<template #default>
<ElTableColumn label="工单编号" prop="code" />
<ElTableColumn label="问题分类" prop="categorie_id">
<template #default="scope">
<div v-html="scope.row.categorie ? scope.row.categorie.name : ''"></div>
</template>
</ElTableColumn>
<ElTableColumn label="问题描述" prop="content" min-width="200px">
<template #default="scope">
<div class="multi-line-ellipsis" v-html="scope.row.content"></div>
</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status == 1 ? 'info' : scope.row.status == 2 ?'primary' :scope.row.status == 4 ?'success' : 'danger'">
{{ scope.row.status == 1 ? '待受理' : scope.row.status == 2 ? '处理中' :
scope.row.status == 4 ? "已完成":'关闭' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" max-width="200px">
<template #default="scope">
<ElRow>
<el-button
v-if="scope.row.status == 1 && hasAuth('work_order_self@edit')"
link
@click="showDialog('edit', scope.row)"
>
<el-button type="primary" link>
<el-icon><Paperclip /></el-icon>
添加附件
</el-button>
</el-upload>
<!-- 已上传附件预览 -->
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
<img
:src="file.file_url || file.response?.data?.file_url"
:alt="file.name"
class="preview-thumb"
/>
</template>
<template v-else>
<el-icon class="file-icon"><Document /></el-icon>
</template>
<span class="file-name">{{ file.name }}</span>
<el-button
type="danger"
size="small"
link
@click="removeChatFile(idx)"
编辑
</el-button>
<el-button
v-if="hasAuth('work_order_self@status')"
link
@click="statusEdit(scope.row)"
>
状态编辑
</el-button>
<el-button
v-if="hasAuth('work_order_self@conversation_list')"
link
@click="openChat(scope.row)"
>
查看/回复
</el-button>
<!-- <el-button link @click="deleteRole()"> 删除 </el-button> -->
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
width="60%"
align-center
>
<ElForm ref="formRef" :model="form" label-width="120px" style="padding-right: 30px">
<ElFormItem label="问题分类" prop="categorie_id">
<ElSelect v-model="form.categorie_id">
<ElOption v-for="type in categorieSelectList" :key="type.id" :value="type.id" :label="type.name" />
</ElSelect>
</ElFormItem>
<ElFormItem label="问题描述" prop="content">
<!-- <ElInput v-model="form.des" type="textarea" :rows="3" /> -->
<!-- 富文本编辑器 -->
<ArtWangEditor class="el-top" v-model="editorHtml" />
</ElFormItem>
<ElFormItem label="上传附件" prop="attachment">
<ElUpload
v-model:file-list="fileList"
class="upload-demo"
:action="actionImageUplode"
:headers="uploadHeaders"
multiple
:limit="3"
:on-exceed="handleExceed"
>
<el-button type="primary" :disabled="fileList.length >= 3">上传附件</el-button>
</ElUpload>
<!-- <ElUpload
v-model:file-list="fileList"
class="upload-demo"
:action="actionImageUplode"
:on-success="handleUploadSuccess"
:headers="uploadHeaders"
multiple
:limit="3"
>
<el-button type="primary">上传附件</el-button>
</ElUpload> -->
</ElFormItem>
<ElFormItem label="" prop="">
<!-- 附件预览区 -->
<div class="attachment-preview">
<div v-if="fileList.length" class="attachment-list">
<div v-for="(file, idx) in fileList" :key="file.name" class="attachment-item">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
<img
:src="file.file_url ? file.file_url : file.response.data.file_url"
:alt="file.name"
class="attachment-thumb"
@click="
handleImageClick(
imageList.findIndex((url) =>
url === file.file_url ? file.file_url : file.response.data.file_url
)
)
"
/>
</template>
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(file.name)">
<video
:src="file.file_url ? file.file_url : file.response.data.file_url"
controls
class="attachment-thumb"
@click.prevent="
handleVideoClick(file.file_url ? file.file_url : file.response.data.file_url)
"
></video>
</template>
<template v-else>
<a
:href="file.file_url ? file.file_url : file.response.data.file_url"
target="_blank"
>{{ file.name }}</a
>
<el-icon><Close /></el-icon>
</el-button>
</template>
<div class="attachment-filename">{{ file.name }}</div>
</div>
</div>
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<el-drawer v-model="isDrawerVisible" :size="isMobile ? '100%' : '480px'" :with-header="false">
<div class="chat-container">
<div class="header">
<div class="header-left">
<span class="name">工单记录</span>
</div>
<div class="header-right">
<el-icon class="icon-close" :size="20" @click="closeChat">
<Close />
</el-icon>
</div>
</div>
<!-- 聊天消息区域 -->
<div class="chat-messages" ref="messageContainer">
<template v-for="(message, index) in messages" :key="index">
<div :class="['message-item', String(message.user_id) === String(userid) ? 'message-right' : 'message-left']">
<el-avatar :size="32" :src="message.user_avatar" class="message-avatar" />
<div class="message-content">
<div class="message-info">
<span class="sender-name">{{ message.user_name }}</span>
<span class="message-time">{{ message.updated_at }}</span>
</div>
<div class="message-text" v-html="message.content"></div>
<!-- 附件显示区域 -->
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
<img
:src="attachment.storage_path_full"
:alt="attachment.original_name"
class="attachment-thumb"
@click="handleChatImageClick(attachment.storage_path_full)"
/>
</template>
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
<video
:src="attachment.storage_path_full"
controls
class="attachment-thumb"
@click.prevent="handleChatVideoClick(attachment.storage_path_full)"
></video>
</template>
<template v-else>
<div class="file-attachment">
<el-icon class="file-icon"><Document /></el-icon>
<span class="file-name">{{ attachment.original_name }}</span>
<el-button
type="primary"
size="small"
link
@click="downloadAttachment(attachment)"
>
下载
</el-button>
</div>
</template>
</div>
</div>
</div>
</div>
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none"
@keyup.enter.prevent="sendMessage" />
<ElInput v-model="ticket_id" type="hidden" />
<div class="chat-input-actions">
<el-button type="primary" @click="sendMessage" v-ripple>发送</el-button>
</template>
</div>
<div class="chat-input" v-if="hasAuth('work_order_self@conversation_add')">
<!-- 附件上传区域 -->
<div class="chat-attachment-upload">
<el-upload
v-model:file-list="chatFileList"
class="chat-upload"
:action="actionImageUplode"
:on-success="handleChatUploadSuccess"
:headers="uploadHeaders"
multiple
:limit="5"
:on-exceed="handleChatExceed"
:show-file-list="false"
>
<el-button type="primary" link>
<el-icon><Paperclip /></el-icon>
添加附件
</el-button>
</el-upload>
<!-- 已上传附件预览 -->
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
<img
:src="file.file_url || file.response?.data?.file_url"
:alt="file.name"
class="preview-thumb"
/>
</template>
<template v-else>
<el-icon class="file-icon"><Document /></el-icon>
</template>
<span class="file-name">{{ file.name }}</span>
<el-button
type="danger"
size="small"
link
@click="removeChatFile(idx)"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</el-drawer>
<ElDialog v-model="dialogVisiblestatus" title="工单状态" width="40%" align-center>
<ElForm ref="formRef" :model="statusform" label-width="120px">
<ElFormItem label="状态" prop="status">
<ElSelect v-model="statusform.status">
<ElOption v-for="type in statusList" :key="type.value" :value="type.value" :label="type.text" />
</ElSelect>
</ElFormItem>
<ElFormItem label="描述" prop="msg">
<ElInput v-model="statusform.msg" type="textarea" :rows="3" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisiblestatus = false">取消</ElButton>
<ElButton type="primary" @click="statusSubmit()">提交</ElButton>
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none"
@keyup.enter.prevent="sendMessage" />
<ElInput v-model="ticket_id" type="hidden" />
<div class="chat-input-actions">
<el-button type="primary" @click="sendMessage" v-ripple>发送</el-button>
</div>
</template>
</ElDialog>
</div>
<!-- 图片预览弹窗 -->
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList.length > 0 ? chatImageList : imageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
<!-- 视频预览弹窗 -->
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
</ElDialog>
</div>
<div v-else class="mobile-hide-tip">
<p>该页面暂不支持移动端访问</p>
</div>
</div>
</el-drawer>
<ElDialog v-model="dialogVisiblestatus" title="工单状态" width="40%" align-center>
<ElForm ref="formRef" :model="statusform" label-width="120px">
<ElFormItem label="状态" prop="status">
<ElSelect v-model="statusform.status">
<ElOption v-for="type in statusList" :key="type.value" :value="type.value" :label="type.text" />
</ElSelect>
</ElFormItem>
<ElFormItem label="描述" prop="msg">
<ElInput v-model="statusform.msg" type="textarea" :rows="3" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisiblestatus = false">取消</ElButton>
<ElButton type="primary" @click="statusSubmit()">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 图片预览弹窗 -->
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList.length > 0 ? chatImageList : imageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
<!-- 视频预览弹窗 -->
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
</ElDialog>
</div>
</template>
@@ -329,8 +324,8 @@
const { uploadHeaders } = useUploadHeaders()
const { info } = useUserStore()
const userid = info.user.id
const { hasAuth } = useAuth();
defineOptions({ name: 'Order' })
const { hasAuth } = useAuth()
defineOptions({ name: 'MyOrder' })
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
const loading = ref(false)
const editorHtml = ref('') // 编辑器内容
@@ -816,6 +811,7 @@
.message-text {
padding: 10px 14px;
line-height: 1.4;
white-space: pre-line;
}
}
}
File diff suppressed because it is too large Load Diff
+177 -186
View File
@@ -1,195 +1,186 @@
<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 { WorkService } from '@/api/workApi'
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[]>([])
<div class="page-content">
<ElRow>
<!-- <ElCol :xs="24" :sm="12" :lg="6">
<ElInput placeholder="名称"></ElInput>
</ElCol> -->
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<!-- <ElButton v-ripple>搜索</ElButton> -->
<ElButton @click="showDialog('add')" v-ripple>新增</ElButton>
</ElCol>
</ElRow>
// 表格实例引用
const tableRef = ref()
onMounted(() => {
<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="操作" max-width="160px">
<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>
</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 { WorkService } from '@/api/workApi'
defineOptions({ name: 'OrderType ' })
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 { data, current, size, total } = await WorkService.getCategorieList({
current: currentPage,
size: pageSize
})
tableData.value = data.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 WorkService.Workcategoriedelete({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
}
const { data, current, size, total } = await WorkService.getCategorieList({
current: currentPage,
size: pageSize
})
tableData.value = data.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 WorkService.Workcategoriedelete({
id: row.id,
})
ElMessage.success(message)
getWorkList()
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
if (dialogType.value === 'add') {
const { message } = await WorkService.WorkcategorieAdd({
name: form.name
})
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
} else {
const { message } = await WorkService.Workcategorieedit({
name: form.name,
id: form.id
})
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
if(dialogType.value === 'add'){
const { message } = await WorkService.WorkcategorieAdd({
name: form.name,
})
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
}else{
const { message } = await WorkService.Workcategorieedit({
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;
}
}
</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>