Squashed commit of the following:
commit9e260ddcd8Author: 浅念 <email@sample.com> Date: Thu Sep 18 09:49:33 2025 +0800 修改 kpi commit6feb3ace75Author: 浅念 <email@sample.com> Date: Thu Sep 18 09:42:44 2025 +0800 kpi 统计 commitec257a246dAuthor: 浅念 <email@sample.com> Date: Wed Sep 17 17:04:35 2025 +0800 修改 弹框表格 commitfa764852abAuthor: 浅念 <email@sample.com> Date: Wed Sep 17 15:54:35 2025 +0800 添加 账户简称 commit65f7f4a123Author: 浅念 <email@sample.com> Date: Wed Sep 17 15:10:28 2025 +0800 品牌加热门 修改 仪表盘接口 commit922cfec81dAuthor: 浅念 <email@sample.com> Date: Tue Sep 16 15:57:17 2025 +0800 调整备货单 crm 端页面 commit5c8df36f12Author: 浅念 <email@sample.com> Date: Mon Sep 15 17:14:38 2025 +0800 调整备货单 及各页面名称 commit5503a14408Author: 浅念 <email@sample.com> Date: Fri Sep 12 18:10:03 2025 +0800 账户添加公司 commit3439b45d9aAuthor: 浅念 <email@sample.com> Date: Fri Sep 12 17:53:26 2025 +0800 修改终版备货单 commit045d13eb7bAuthor: 浅念 <email@sample.com> Date: Fri Sep 12 17:49:07 2025 +0800 修改备货单 等页面 commitda690dfa06Author: 浅念 <email@sample.com> Date: Thu Sep 11 17:44:37 2025 +0800 修改 备货单 详情 commit853e9591b3Author: 浅念 <email@sample.com> Date: Wed Sep 10 18:11:39 2025 +0800 修改 备货单页面 commit9052bd1027Author: 浅念 <email@sample.com> Date: Tue Sep 9 20:53:30 2025 +0800 备货单 审核功能 commit079d9babb8Merge:ebd747991d810bAuthor: 浅念 <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 commitebd7479573Merge:64e53b6459d306Author: 浅念 <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 commit64e53b6ee9Author: 浅念 <email@sample.com> Date: Mon Sep 8 19:09:55 2025 +0800 备货单 加 审批流程 commit9bd7c89cb4Author: 浅念 <email@sample.com> Date: Thu Sep 4 15:09:17 2025 +0800 车辆信息,备货单 等页面
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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"></i> 查看审批
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.status == 'pending'"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleCreateRepair(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></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"></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"></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"></i>
|
||||
<i v-if="rec.rejected" class="status-dot rejected"></i>
|
||||
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys"></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>
|
||||
@@ -167,6 +167,7 @@
|
||||
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)
|
||||
|
||||
@@ -257,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()
|
||||
|
||||
@@ -341,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()
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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"></i> 打印预览
|
||||
</el-link>
|
||||
<el-link type="primary" underline="never" @click="handleView1(scope.row)">
|
||||
<i class="iconfont-sys"></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"></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"></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>
|
||||
@@ -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"></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"></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>
|
||||
|
||||
|
||||
@@ -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"></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"></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"></i> 打印预览
|
||||
</el-link>
|
||||
<el-link
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.is_use == 1"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleView1(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></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"></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"></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>
|
||||
|
||||
@@ -93,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>()
|
||||
|
||||
@@ -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>
|
||||
@@ -148,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>()
|
||||
|
||||
@@ -183,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>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'Type ' })
|
||||
defineOptions({ name: 'AdminNotice' })
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogVisiblexq = ref(false)
|
||||
|
||||
@@ -155,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')
|
||||
|
||||
@@ -410,6 +410,7 @@
|
||||
import { systemService } from '@/api/systemApi'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
defineOptions({ name: 'UserList' })
|
||||
const { hasAuth } = useAuth()
|
||||
const activeName = ref('1')
|
||||
const selectValue = ref('1')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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'"
|
||||
|
||||
@@ -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>
|
||||
@@ -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"></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"></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"></i>
|
||||
<i v-if="rec.rejected" class="status-dot rejected"></i>
|
||||
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys"></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>
|
||||
@@ -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"></i> 查看审批
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.status == 'pending'"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleCreateRepair(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></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>
|
||||
@@ -122,6 +122,7 @@
|
||||
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()
|
||||
|
||||
@@ -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' }],
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
const levelOptions = ref<any>()
|
||||
const followDesc = ref('')
|
||||
const activecolor = '#3471FF'
|
||||
defineOptions({ name: 'CustomerFollowAdd' })
|
||||
|
||||
const successDialogVisible = ref(false) // 控制弹窗显示
|
||||
const remindValue = ref('1')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"> </i>
|
||||
<i v-else class="iconfont-sys"></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"> </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"> </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"></i> 跟进
|
||||
</el-link>
|
||||
<el-link
|
||||
type="warning"
|
||||
underline="never"
|
||||
@click="reportfn(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></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"></i> 移除
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
underline="never"
|
||||
@click="crmdelectfn(scope.row)"
|
||||
v-if="hasAuth('crm_customer@delete')"
|
||||
>
|
||||
<i class="iconfont-sys"></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>
|
||||
@@ -387,6 +387,7 @@
|
||||
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('')
|
||||
|
||||
@@ -79,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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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"></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"></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"></i> 提交
|
||||
</el-link>
|
||||
<el-link type="primary" underline="never" @click="handleView1(scope.row)">
|
||||
<i class="iconfont-sys"></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"></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"></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>
|
||||
@@ -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"></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"></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>
|
||||
|
||||
|
||||
@@ -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"></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"></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
@@ -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"></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"></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"></i> 编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></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"></i> 创建终版
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.is_use == 1"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleView1(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看终版
|
||||
</el-link>
|
||||
<el-link type="warning" underline="never" @click="copys(scope.row)">
|
||||
<i class="iconfont-sys"></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"></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"></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>
|
||||
@@ -100,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({
|
||||
|
||||
@@ -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>('')
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -324,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('') // 编辑器内容
|
||||
|
||||
+149
-70
@@ -11,26 +11,51 @@
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
<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
|
||||
: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>
|
||||
@@ -38,9 +63,23 @@
|
||||
<ElTableColumn fixed="right" label="操作" max-width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button v-if="scope.row.status == 1&&hasAuth('work_order@edit')" link @click="showDialog('edit', scope.row)"> 编辑 </el-button>
|
||||
<el-button v-if="hasAuth('work_order@status')" link @click="statusEdit(scope.row)"> 状态编辑 </el-button>
|
||||
<el-button v-if="hasAuth('work_order@conversation_list')" link @click="openChat(scope.row)"> 查看/回复 </el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status == 1 && hasAuth('work_order@edit')"
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="hasAuth('work_order@status')" link @click="statusEdit(scope.row)">
|
||||
状态编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAuth('work_order@conversation_list')"
|
||||
link
|
||||
@click="openChat(scope.row)"
|
||||
>
|
||||
查看/回复
|
||||
</el-button>
|
||||
<!-- <el-button link @click="deleteRole()"> 删除 </el-button> -->
|
||||
</ElRow>
|
||||
</template>
|
||||
@@ -48,11 +87,21 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" width="60%" align-center>
|
||||
<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" />
|
||||
<ElOption
|
||||
v-for="type in categorieSelectList"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
:label="type.name"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="问题描述" prop="content">
|
||||
@@ -80,13 +129,35 @@
|
||||
<div 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))" />
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -122,12 +193,8 @@
|
||||
String(message.user_id) === String(userid) ? 'message-right' : 'message-left'
|
||||
]"
|
||||
>
|
||||
<!-- :src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar" -->
|
||||
<el-avatar
|
||||
:size="32"
|
||||
:src="message.user_avatar"
|
||||
class="message-avatar"
|
||||
/>
|
||||
<!-- :src="String(message.user_id) === String(userid) ? meAvatar : aiAvatar" -->
|
||||
<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>
|
||||
@@ -135,20 +202,29 @@
|
||||
</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"
|
||||
<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
|
||||
<video
|
||||
:src="attachment.storage_path_full"
|
||||
controls
|
||||
class="attachment-thumb"
|
||||
@click.prevent="handleChatVideoClick(attachment.storage_path_full)"
|
||||
></video>
|
||||
@@ -157,10 +233,10 @@
|
||||
<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
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
@click="downloadAttachment(attachment)"
|
||||
>
|
||||
下载
|
||||
@@ -196,9 +272,9 @@
|
||||
<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"
|
||||
<img
|
||||
:src="file.file_url || file.response?.data?.file_url"
|
||||
:alt="file.name"
|
||||
class="preview-thumb"
|
||||
/>
|
||||
</template>
|
||||
@@ -206,12 +282,7 @@
|
||||
<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 type="danger" size="small" link @click="removeChatFile(idx)">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -229,7 +300,6 @@
|
||||
<el-button type="primary" @click="sendMessage" v-ripple>发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
@@ -237,7 +307,12 @@
|
||||
<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" />
|
||||
<ElOption
|
||||
v-for="type in statusList"
|
||||
:key="type.value"
|
||||
:value="type.value"
|
||||
:label="type.text"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="msg">
|
||||
@@ -253,10 +328,21 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 图片预览弹窗 -->
|
||||
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList.length > 0 ? chatImageList : imageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
|
||||
<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>
|
||||
<video
|
||||
v-if="currentVideoUrl"
|
||||
:src="currentVideoUrl"
|
||||
controls
|
||||
autoplay
|
||||
style="width: 100%; max-height: 70vh; display: block"
|
||||
></video>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -279,7 +365,6 @@
|
||||
const editorHtml = ref('') // 编辑器内容
|
||||
const dialogVisible = ref(false)
|
||||
const dialogVisiblestatus = ref(false)
|
||||
const typeList = ref<any[]>([])
|
||||
const formRef = ref<FormInstance>()
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
@@ -288,10 +373,6 @@
|
||||
const { info } = useUserStore()
|
||||
const userid = info.user.id
|
||||
console.log(userid)
|
||||
import meAvatar from '@/assets/img/avatar/avatar5.webp'
|
||||
import aiAvatar from '@/assets/img/avatar/avatar10.webp'
|
||||
|
||||
|
||||
// 消息相关数据
|
||||
const messages = ref<any[]>([])
|
||||
const messageText = ref('')
|
||||
@@ -386,8 +467,8 @@
|
||||
}
|
||||
|
||||
//获取工单记录 getconversationList
|
||||
const ticket_id =ref()
|
||||
const getConversationList = async (id:any) => {
|
||||
const ticket_id = ref()
|
||||
const getConversationList = async (id: any) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = paginationmsg
|
||||
@@ -412,19 +493,19 @@
|
||||
const text = messageText.value.trim()
|
||||
const ids = ticket_id.value
|
||||
if (!text && chatFileList.value.length === 0) return
|
||||
|
||||
// 准备附件数据
|
||||
const attachments = chatFileList.value.map(file => {
|
||||
const data = file.response?.data
|
||||
if (data) {
|
||||
return {
|
||||
original_name: data.original_name,
|
||||
storage_path: data.storage_path || data.file_name
|
||||
const attachments = chatFileList.value
|
||||
.map((file) => {
|
||||
const data = file.response?.data
|
||||
if (data) {
|
||||
return {
|
||||
original_name: data.original_name,
|
||||
storage_path: data.storage_path || data.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}).filter(Boolean)
|
||||
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
const { message } = await WorkService.conversationAdd({
|
||||
content: text,
|
||||
ticket_id: ids,
|
||||
@@ -435,9 +516,7 @@
|
||||
chatFileList.value = [] // 清空附件列表
|
||||
getConversationList(ids)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
|
||||
+171
-175
@@ -1,190 +1,186 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<!-- <ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="名称"></ElInput>
|
||||
</ElCol> -->
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<!-- <ElButton v-ripple>搜索</ElButton> -->
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增</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">
|
||||
<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>
|
||||
|
||||
<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: '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[]>([])
|
||||
<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>
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user