收据设置,招商人员

This commit is contained in:
zengqiyang 2025-04-11 16:14:49 +08:00
parent 50a376b14b
commit 12b6d3fe79
21 changed files with 5611 additions and 25 deletions

View File

@ -1,5 +1,9 @@
{
"dependencies": {
"html2canvas": "^1.4.1",
"mammoth": "^1.9.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.1.91",
"qrcode.vue": "^3.6.0"
}
}

View File

@ -1,4 +1,4 @@
NODE_ENV = 'development'
# 开发环境API地址
VUE_APP_BASE_API = http://192.168.137.3:8080/api
VUE_APP_BASE_API = http://192.168.137.38:8080

View File

@ -90,4 +90,171 @@ export function deleteFeeType(id) {
url: `/finance/type/${id}`,
method: 'delete'
})
}
// 收据编号规则相关接口
export function listReceiptRule(params) {
return request({
url: '/receipt/number-rule/page',
method: 'get',
params
})
}
export function addReceiptRule(data) {
return request({
url: '/receipt/number-rule',
method: 'post',
data
})
}
export function deleteReceiptRule(id) {
return request({
url: `/receipt/number-rule/${id}`,
method: 'delete'
})
}
export function getNextReceiptNumber(ruleId) {
return request({
url: `/receipt/number/next/${ruleId}`,
method: 'get'
})
}
export function getBatchReceiptNumbers(ruleId, count) {
return request({
url: `/receipt/numbers/${ruleId}/${count}`,
method: 'get'
})
}
// 收据模板相关接口
export function getReceiptTemplateList(params) {
return request({
url: '/receipt/template/page',
method: 'get',
params
})
}
export function addReceiptTemplate(data) {
return request({
url: '/receipt/template',
method: 'post',
data
})
}
export function updateReceiptTemplate(id, data) {
return request({
url: `/receipt/template/${id}`,
method: 'put',
data
})
}
export function deleteReceiptTemplate(id) {
return request({
url: `/receipt/template/${id}`,
method: 'delete'
})
}
export function previewReceiptTemplate(id) {
return request({
url: `/receipt/template/preview/${id}`,
method: 'get',
responseType: 'blob'
})
}
export function downloadReceiptTemplate(id) {
return request({
url: `/receipt/template/download/${id}`,
method: 'get',
responseType: 'blob'
})
}
export function downloadExampleTemplate() {
return request({
url: '/receipt/template/download-example',
method: 'get',
responseType: 'blob'
})
}
export function getReceiptKeywords() {
return request({
url: '/receipt/template/keywords',
method: 'get'
})
}
export function checkTemplateName(params) {
return request({
url: '/receipt/template/check-name',
method: 'get',
params
})
}
// 收款方信息相关接口
export function getPayeeInfo(params) {
if (typeof params === 'object') {
// 查询列表
return request({
url: '/receipt/payee/page',
method: 'get',
params
})
} else {
// 查询详情
return request({
url: `/receipt/payee/${params}`,
method: 'get'
})
}
}
export function addPayeeInfo(data) {
return request({
url: '/receipt/payee',
method: 'post',
data
})
}
export function updatePayeeInfo(id, data) {
return request({
url: `/receipt/payee/${id}`,
method: 'put',
data
})
}
export function deletePayeeInfo(id) {
return request({
url: `/receipt/payee/${id}`,
method: 'delete'
})
}
// 项目列表
export function getProjectList() {
return request({
url: '/project/list',
method: 'get'
})
}
// 获取楼宇列表
export function getBuildingList(projectId) {
return request({
url: `/business/building/list/${projectId}`,
method: 'get'
})
}

255
pc/src/api/merchant.js Normal file
View File

@ -0,0 +1,255 @@
import request from '@/utils/request'
// 招商团队相关API
export function getMerchantTeamList(params) {
return request({
url: '/business/team/list',
method: 'get',
params
})
}
export function addMerchantTeam(data) {
return request({
url: '/business/team',
method: 'post',
data
})
}
export function updateMerchantTeam(data) {
return request({
url: '/business/team',
method: 'put',
data
})
}
export function deleteMerchantTeam(id) {
return request({
url: `/business/team/${id}`,
method: 'delete'
})
}
// 招商人员相关API
export function getMerchantPersonnelList(params) {
return request({
url: '/business/personnel/list',
method: 'get',
params
})
}
export function getPersonnelByTeamId(teamId) {
return request({
url: `/business/personnel/team/${teamId}`,
method: 'get'
})
}
export function getPersonnelDetail(id) {
return request({
url: `/business/personnel/${id}`,
method: 'get'
})
}
export function addMerchantPersonnel(data) {
return request({
url: '/business/personnel',
method: 'post',
data
})
}
export function removeMerchantPersonnel(id) {
return request({
url: `/business/personnel/${id}`,
method: 'delete'
})
}
// 成员管理相关API
export function getDepartmentTree() {
return request({
url: '/business/member/dept/tree',
method: 'get'
})
}
export function getDepartmentMemberTree() {
return request({
url: '/business/member/tree',
method: 'get'
})
}
export function getManagerList() {
return request({
url: '/business/member/managers',
method: 'get'
})
}
export function getMemberList(params) {
return request({
url: '/business/member/list',
method: 'get',
params
})
}
export function getMembersByDeptId(deptId) {
return request({
url: `/business/member/dept/${deptId}`,
method: 'get'
})
}
export function getMemberDetail(memberId) {
return request({
url: `/business/member/${memberId}`,
method: 'get'
})
}
// 获取项目列表
export function getProjectList() {
return request({
url: '/project/list',
method: 'get'
})
}
// 获取线索数量
export function getClueCount(personnelId) {
return request({
url: `/merchant/clue/count/${personnelId}`,
method: 'get'
})
}
// 获取意向客户数量
export function getIntentCustomerCount(personnelId) {
return request({
url: `/merchant/intent-customer/count/${personnelId}`,
method: 'get'
})
}
// 获取合同数量
export function getContractCount(personnelId) {
return request({
url: `/merchant/contract/count/${personnelId}`,
method: 'get'
})
}
// 标签分组相关API
export function getTagGroupList(params) {
return request({
url: '/business/tag-group/list',
method: 'get',
params
})
}
export function getAllTagGroups() {
return request({
url: '/business/tag-group/all',
method: 'get'
})
}
export function getTagGroupDetail(id) {
return request({
url: `/business/tag-group/${id}`,
method: 'get'
})
}
export function addTagGroup(data) {
return request({
url: '/business/tag-group',
method: 'post',
data
})
}
export function updateTagGroup(data) {
return request({
url: '/business/tag-group',
method: 'put',
data
})
}
export function deleteTagGroup(id) {
return request({
url: `/business/tag-group/${id}`,
method: 'delete'
})
}
export function checkTagGroupName(params) {
return request({
url: '/business/tag-group/check-name',
method: 'get',
params
})
}
// 标签相关API
export function getTagList(params) {
return request({
url: '/business/tag/list',
method: 'get',
params
})
}
export function getTagListByGroupId(groupId) {
return request({
url: `/business/tag/group/${groupId}`,
method: 'get'
})
}
export function getTagDetail(id) {
return request({
url: `/business/tag/${id}`,
method: 'get'
})
}
export function addTag(data) {
return request({
url: '/business/tag',
method: 'post',
data
})
}
export function updateTag(data) {
return request({
url: '/business/tag',
method: 'put',
data
})
}
export function deleteTag(id) {
return request({
url: `/business/tag/${id}`,
method: 'delete'
})
}
export function checkTagName(params) {
return request({
url: '/business/tag/check-name',
method: 'get',
params
})
}

View File

@ -0,0 +1,655 @@
<template>
<el-dialog
title="请选择成员"
:visible.sync="visible"
width="550px"
class="member-dialog"
append-to-body
@close="handleClose"
>
<div class="member-dialog-content">
<!-- 左侧部分 -->
<div class="member-list-area">
<div class="member-search-header">
<el-select v-model="memberType" placeholder="成员" class="member-type-select">
<el-option label="成员" value="member"></el-option>
</el-select>
<el-input
v-model="memberSearchKeyword"
placeholder="请输入姓名"
class="search-input"
prefix-icon="el-icon-search"
clearable
/>
</div>
<div class="member-list">
<div class="all-select">
<el-checkbox
v-model="selectAll"
:disabled="selectionMode === 'single'"
@change="handleSelectAllChange"
>全选</el-checkbox>
</div>
<div v-for="department in departmentList" :key="department.id" class="department-item">
<div class="department-name" @click="toggleDepartment(department)">
<i class="el-icon-user-solid department-icon"></i>
{{ department.name }}
<i :class="['el-icon-arrow-right department-expand-icon', {'is-expanded': department.expanded}]"></i>
</div>
<div v-show="department.expanded">
<!-- 部门直属成员 -->
<template v-if="selectionMode === 'single'">
<div v-for="member in filteredMembers(department.id)" :key="member.id" class="member-item">
<el-radio
v-model="tempSelectedMemberId"
:label="member.id"
@change="handleMemberSelected(member, department)"
>{{ member.name }}</el-radio>
</div>
</template>
<template v-else>
<div v-for="member in filteredMembers(department.id)" :key="member.id" class="member-item">
<el-checkbox
v-model="member.checked"
@change="(val) => handleMemberCheckChange(val, member, department)"
>{{ member.name }}</el-checkbox>
</div>
</template>
<!-- 子部门 -->
<div v-if="department.children && department.children.length">
<div v-for="subDept in department.children" :key="subDept.id" class="department-item sub-dept">
<div class="department-name" @click.stop="toggleDepartment(subDept)">
<i class="el-icon-office-building department-icon"></i>
{{ subDept.name }}
<i :class="['el-icon-arrow-right department-expand-icon', {'is-expanded': subDept.expanded}]"></i>
</div>
<div v-show="subDept.expanded">
<template v-if="selectionMode === 'single'">
<div v-for="member in filteredMembers(subDept.id)" :key="member.id" class="member-item">
<el-radio
v-model="tempSelectedMemberId"
:label="member.id"
@change="handleMemberSelected(member, subDept)"
>{{ member.name }}</el-radio>
</div>
</template>
<template v-else>
<div v-for="member in filteredMembers(subDept.id)" :key="member.id" class="member-item">
<el-checkbox
v-model="member.checked"
@change="(val) => handleMemberCheckChange(val, member, subDept)"
>{{ member.name }}</el-checkbox>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧已选择部分 -->
<div class="selected-area">
<div class="selected-header">
已选择<span class="clear-btn" @click="clearSelectedMembers">清空</span>
</div>
<div class="selected-content">
<template v-if="selectionMode === 'single'">
<div v-if="tempSelectedMember" class="selected-item">
<div class="member-avatar">{{ tempSelectedMember.name.slice(-1) }}</div>
<span class="member-name">{{ tempSelectedMember.name }}</span>
<i class="el-icon-close" @click="clearSelectedMembers"></i>
</div>
<div v-else class="no-selected">未选择</div>
</template>
<template v-else>
<div v-if="tempSelectedMembers.length > 0">
<div v-for="member in tempSelectedMembers" :key="member.id" class="selected-item">
<div class="member-avatar">{{ member.name.slice(-1) }}</div>
<span class="member-name">{{ member.name }}</span>
<i class="el-icon-close" @click="removeTempSelectedMember(member)"></i>
</div>
</div>
<div v-else class="no-selected">未选择</div>
</template>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel"> </el-button>
<el-button type="primary" @click="handleConfirm"> </el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'MemberSelector',
props: {
visible: {
type: Boolean,
default: false
},
//
selectionMode: {
type: String,
default: 'single', // 'single' 'multiple'
validator: (value) => ['single', 'multiple'].includes(value)
},
//
selectedMember: {
type: Object,
default: () => null
},
selectedMembers: {
type: Array,
default: () => []
},
//
departments: {
type: Array,
default: () => []
},
//
members: {
type: Array,
default: () => []
}
},
data() {
return {
memberType: 'member',
memberSearchKeyword: '',
selectAll: false,
//
tempSelectedMemberId: null,
tempSelectedMember: null,
tempSelectedMembers: [],
// 使props
departmentList: [],
allMembers: []
}
},
computed: {
//
isAllSelected() {
if (this.allMembers.length === 0) return false
return this.tempSelectedMembers.length === this.allMembers.length
}
},
watch: {
visible(val) {
if (val) {
this.initData()
}
},
selectedMember: {
handler(val) {
if (this.selectionMode === 'single' && val) {
this.tempSelectedMember = { ...val }
this.tempSelectedMemberId = val.id
// selectedMemberradio
this.$nextTick(() => {
if (val.id) {
//
this.tempSelectedMemberId = val.id;
//
console.log('selectedMember updated:', val.name, 'ID:', val.id);
}
});
}
},
immediate: true,
deep: true
},
selectedMembers: {
handler(val) {
if (this.selectionMode === 'multiple' && val.length > 0) {
this.tempSelectedMembers = [...val]
}
},
immediate: true
},
//
tempSelectedMembers: {
handler(val) {
if (this.selectionMode === 'multiple') {
this.selectAll = this.isAllSelected
}
},
deep: true
},
departments: {
handler(val) {
if (val.length > 0) {
this.departmentList = JSON.parse(JSON.stringify(val))
//
this.departmentList.forEach(dept => {
this.$set(dept, 'expanded', true)
if (dept.children) {
dept.children.forEach(child => {
this.$set(child, 'expanded', false)
})
}
})
}
},
immediate: true
},
members: {
handler(val) {
if (val.length > 0) {
this.allMembers = JSON.parse(JSON.stringify(val))
// checked
this.resetMembersCheckStatus()
}
},
immediate: true
}
},
methods: {
//
initData() {
//
this.memberSearchKeyword = ''
console.log('MemberSelector initData called, selectedMember:', this.selectedMember)
if (this.selectionMode === 'single') {
//
if (this.selectedMember) {
this.tempSelectedMember = { ...this.selectedMember }
this.tempSelectedMemberId = this.selectedMember.id
console.log('单选模式设置tempSelectedMemberId:', this.tempSelectedMemberId)
//
this.tempSelectedMembers = [{ ...this.selectedMember }]
// radio
this.$nextTick(() => {
//
if (this.tempSelectedMemberId && this.allMembers.length > 0) {
//
const member = this.allMembers.find(m => m.id == this.tempSelectedMemberId);
if (member) {
member.checked = true;
this.$set(member, 'checked', true);
console.log('单选模式已选择成员:', member.name, 'ID:', this.tempSelectedMemberId, 'checked:', member.checked);
}
}
});
} else {
this.tempSelectedMember = null
this.tempSelectedMemberId = null
this.tempSelectedMembers = []
}
this.selectAll = false
} else {
//
this.tempSelectedMember = null
this.tempSelectedMemberId = null
this.tempSelectedMembers = [...this.selectedMembers]
}
//
this.departmentList.forEach(dept => {
dept.expanded = true
if (dept.children) {
dept.children.forEach(child => {
child.expanded = false
})
}
})
//
this.resetMembersCheckStatus()
//
if (this.selectionMode === 'multiple' && this.tempSelectedMembers.length > 0) {
this.setMembersCheckedByIds(this.tempSelectedMembers.map(m => m.id))
//
this.selectAll = this.tempSelectedMembers.length === this.allMembers.length
} else if (this.selectionMode === 'single' && this.tempSelectedMemberId) {
// ID
const ids = [this.tempSelectedMemberId];
this.setMembersCheckedByIds(ids);
console.log('单选模式:设置选中状态, ids:', ids)
}
},
//
resetMembersCheckStatus() {
this.allMembers.forEach(member => {
member.checked = false
})
},
// ID
setMembersCheckedByIds(ids) {
this.allMembers.forEach(member => {
member.checked = ids.includes(member.id)
})
},
// ID
filteredMembers(deptId) {
if (this.memberSearchKeyword) {
return this.allMembers.filter(member =>
member.deptId === deptId &&
member.name.includes(this.memberSearchKeyword)
)
}
return this.allMembers.filter(member => member.deptId === deptId)
},
// /
toggleDepartment(department) {
department.expanded = !department.expanded
},
//
handleMemberSelected(member, department) {
console.log('单选成员:', member.name, 'ID:', member.id);
//
this.allMembers.forEach(m => {
m.checked = m.id === member.id;
});
//
this.tempSelectedMember = {
id: member.id,
name: member.name,
phone: member.phone,
department: department.name
}
//
this.tempSelectedMembers = [this.tempSelectedMember];
},
//
handleMemberCheckChange(checked, member, department) {
if (checked) {
//
const exists = this.tempSelectedMembers.some(m => m.id === member.id)
if (!exists) {
this.tempSelectedMembers.push({
id: member.id,
name: member.name,
phone: member.phone,
department: department.name
})
}
} else {
//
const index = this.tempSelectedMembers.findIndex(m => m.id === member.id)
if (index !== -1) {
this.tempSelectedMembers.splice(index, 1)
}
}
},
//
removeTempSelectedMember(member) {
//
const index = this.tempSelectedMembers.findIndex(m => m.id === member.id)
if (index !== -1) {
this.tempSelectedMembers.splice(index, 1)
}
//
const targetMember = this.allMembers.find(m => m.id === member.id)
if (targetMember) {
targetMember.checked = false
}
},
//
clearSelectedMembers() {
this.tempSelectedMembers = []
this.tempSelectedMember = null
this.tempSelectedMemberId = null
//
this.resetMembersCheckStatus()
},
//
handleCancel() {
this.$emit('update:visible', false)
this.$emit('cancel')
},
//
handleClose() {
this.$emit('update:visible', false)
},
//
handleConfirm() {
if (this.selectionMode === 'single') {
//
if (!this.tempSelectedMember) {
this.$message.warning('请选择成员')
return
}
this.$emit('confirm', this.tempSelectedMember)
} else {
//
if (this.tempSelectedMembers.length === 0) {
this.$message.warning('请选择成员')
return
}
this.$emit('confirm', this.tempSelectedMembers)
}
this.$emit('update:visible', false)
},
//
handleSelectAllChange(checked) {
if (this.selectionMode !== 'multiple') return
//
this.allMembers.forEach(member => {
member.checked = checked
})
if (checked) {
// :
this.tempSelectedMembers = this.allMembers.map(member => {
//
let departmentName = ''
for (const dept of this.departmentList) {
if (dept.id === member.deptId) {
departmentName = dept.name
break
}
if (dept.children) {
for (const subDept of dept.children) {
if (subDept.id === member.deptId) {
departmentName = subDept.name
break
}
}
}
}
return {
id: member.id,
name: member.name,
phone: member.phone || '',
department: departmentName
}
})
} else {
// :
this.tempSelectedMembers = []
}
}
}
}
</script>
<style lang="scss" scoped>
.member-dialog {
.member-dialog-content {
display: flex;
height: 350px;
.member-list-area {
width: 65%;
border-right: 1px solid #EBEEF5;
padding-right: 15px;
display: flex;
flex-direction: column;
.member-search-header {
display: flex;
margin-bottom: 10px;
.member-type-select {
width: 90px;
}
.search-input {
flex: 1;
}
}
.department-nav {
border-bottom: 1px solid #EBEEF5;
margin-bottom: 10px;
.nav-item {
display: inline-block;
padding: 8px 0;
cursor: pointer;
color: #409EFF;
}
}
.member-list {
flex: 1;
overflow-y: auto;
.all-select {
padding: 8px 0;
}
.department-item {
margin-bottom: 5px;
&.sub-dept {
padding-left: 20px;
}
.department-name {
display: flex;
align-items: center;
cursor: pointer;
padding: 5px 0;
.department-icon {
margin-right: 5px;
color: #5c6b77;
}
.department-expand-icon {
margin-left: auto;
transition: transform 0.3s;
&.is-expanded {
transform: rotate(90deg);
}
}
}
.member-item {
padding: 3px 0 3px 20px;
}
}
}
}
.selected-area {
width: 35%;
padding-left: 15px;
display: flex;
flex-direction: column;
.selected-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
color: #606266;
.clear-btn {
color: #409EFF;
cursor: pointer;
font-size: 12px;
}
}
.selected-content {
flex: 1;
overflow-y: auto;
.selected-item {
display: flex;
align-items: center;
margin-bottom: 10px;
.member-avatar {
width: 28px;
height: 28px;
background-color: #409EFF;
color: #fff;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
font-size: 14px;
}
.member-name {
flex: 1;
color: #606266;
}
.el-icon-close {
color: #C0C4CC;
cursor: pointer;
&:hover {
color: #F56C6C;
}
}
}
.no-selected {
color: #909399;
text-align: center;
margin-top: 20px;
}
}
}
}
}
</style>

View File

@ -4,6 +4,7 @@ import Layout from '@/layout/index'
import systemRoutes from './modules/system'
import projectRoutes from './modules/project'
import financeRoutes from './modules/finance'
import merchantRoutes from './modules/merchant'
Vue.use(VueRouter)
@ -30,6 +31,7 @@ const routes = [
systemRoutes,
projectRoutes,
financeRoutes,
merchantRoutes,
{
path: '/asset',
component: Layout,

View File

@ -12,6 +12,12 @@ export default {
component: () => import('@/views/finance/feeType/index.vue'),
name: 'FeeType',
meta: { title: '费用类型', icon: 'el-icon-tickets' }
},
{
path: 'receiptSetting',
component: () => import('@/views/finance/receiptSetting/index.vue'),
name: 'ReceiptSetting',
meta: { title: '收据设置', icon: 'el-icon-document' }
}
]
}

View File

@ -0,0 +1,33 @@
export default {
path: '/merchant',
component: () => import('@/layout/index'),
redirect: '/merchant/merchant-personnel',
name: 'Merchant',
meta: { title: '招商管理', icon: 'el-icon-user-solid' },
children: [
{
path: 'merchant-personnel',
component: () => import('@/views/merchant/merchant-personnel/index.vue'),
name: 'MerchantPersonnel',
meta: { title: '招商人员', icon: 'el-icon-user' }
},
{
path: 'tag-management',
component: () => import('@/views/merchant/tag-management/index.vue'),
name: 'TagManagement',
meta: { title: '标签管理', icon: 'el-icon-collection-tag' }
},
{
path: 'clue-management',
component: () => import('@/views/merchant/clue-management/index.vue'),
name: 'ClueManagement',
meta: { title: '线索管理', icon: 'el-icon-chat-line-square' }
},
{
path: 'intent-customer',
component: () => import('@/views/merchant/intent-customer/index.vue'),
name: 'IntentCustomer',
meta: { title: '意向客户', icon: 'el-icon-s-custom' }
}
]
}

View File

@ -0,0 +1,5 @@
// API响应状态码
export const API_SUCCESS_CODE = '0000000000000000'
// 其他常量可以在此添加

View File

@ -1,9 +1,10 @@
import axios from 'axios'
import { Message } from 'element-ui'
import { API_SUCCESS_CODE } from './constants'
// 创建axios实例
const service = axios.create({
// baseURL: '/api', // 修改为相对路径,使用代理
// baseURL: '/', // 修改为相对路径,使用代理
baseURL: process.env.VUE_APP_BASE_API, // 使用环境变量中的接口地址
timeout: 10000 // 请求超时时间
@ -30,8 +31,8 @@ service.interceptors.response.use(
}
const res = response.data
// 如果返回的状态码不是000000,则判断为错误
if (res.code !== '000000') {
// 如果返回的状态码不是成功码,则判断为错误
if (res.code !== API_SUCCESS_CODE) {
Message({
message: res.msg || res.message || '系统错误',
type: 'error',

View File

@ -162,6 +162,7 @@ import {
updateFeeType,
deleteFeeType
} from '@/api/finance'
import { API_SUCCESS_CODE } from '@/utils/constants'
export default {
name: 'FeeTypeManagement',
@ -238,7 +239,7 @@ export default {
//
getCategoryList() {
getAllFeeCategories().then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.categoryList = response.data
if (this.categoryList.length > 0 && !this.selectedCategoryId) {
this.handleCategorySelect(this.categoryList[0].id.toString())
@ -253,7 +254,7 @@ export default {
if (this.categoryQuery) {
const params = { categoryName: this.categoryQuery, pageNum: 1, pageSize: 100 }
getFeeCategories(params).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.categoryList = response.data.list
} else {
this.$message.error(response.message || '查询失败')
@ -275,7 +276,7 @@ export default {
getTypeList() {
this.loading = true
getFeeTypes(this.queryParams).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.typeList = response.data.list
this.total = response.data.total
} else {
@ -329,7 +330,7 @@ export default {
//
handleEditCategory(row) {
getFeeCategory(row.id).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
const categoryData = response.data
//
if (categoryData.feeTpNo !== undefined) {
@ -353,7 +354,7 @@ export default {
if (valid) {
if (this.categoryForm.id) {
updateFeeCategory(this.categoryForm.id, this.categoryForm).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.$message.success('修改成功')
this.categoryOpen = false
this.getCategoryList()
@ -363,7 +364,7 @@ export default {
})
} else {
addFeeCategory(this.categoryForm).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.$message.success('新增成功')
this.categoryOpen = false
this.getCategoryList()
@ -383,7 +384,7 @@ export default {
type: 'warning'
}).then(() => {
deleteFeeCategory(row.id).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.$message.success('删除成功')
this.getCategoryList()
//
@ -423,7 +424,7 @@ export default {
//
handleEditType(row) {
getFeeType(row.id).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
//
const typeData = response.data
//
@ -461,7 +462,7 @@ export default {
if (this.typeForm.id) {
updateFeeType(this.typeForm.id, submitData).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.$message.success('修改成功')
this.typeOpen = false
this.getTypeList()
@ -471,7 +472,7 @@ export default {
})
} else {
addFeeType(submitData).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.$message.success('新增成功')
this.typeOpen = false
this.getTypeList()
@ -491,7 +492,7 @@ export default {
type: 'warning'
}).then(() => {
deleteFeeType(row.id).then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.$message.success('删除成功')
this.getTypeList()
} else {
@ -519,7 +520,7 @@ export default {
//
loadFullCategoryList() {
getAllFeeCategories().then(response => {
if (response.code === '000000') {
if (response.code === API_SUCCESS_CODE) {
this.categoryList = response.data
}
})

View File

@ -0,0 +1,582 @@
<template>
<div class="payee-info-container">
<!-- 搜索栏 -->
<div class="filter-container">
<el-input v-model="queryParams.payeeNameUnitName" placeholder="请输入收款方单位名称" clearable style="width: 250px;" class="filter-item" />
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" icon="el-icon-plus" style="float: right;" @click="handleAdd">新增收款方信息</el-button>
</div>
<!-- 收款方信息列表 -->
<el-table v-loading="loading" :data="payeeList" border style="margin-top: 15px;">
<el-table-column prop="companyId" label="关联公司">
<template slot-scope="scope">
{{ formatCompanyName(scope.row.companyId) }}
</template>
</el-table-column>
<el-table-column prop="payeeNameUnitName" label="收款方单位名称" />
<el-table-column prop="payeeName" label="收款人" />
<el-table-column prop="addr" label="地址" />
<el-table-column prop="contTel" label="电话" />
<el-table-column prop="buildingIds" label="应用楼宇">
<template slot-scope="scope">
{{ formatBuildingNames(scope.row.buildingIds) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="queryParams.current" :page-sizes="[10, 20, 30, 50]" :page-size="queryParams.size"
layout="total, sizes, prev, pager, next, jumper" :total="total" />
</div>
<!-- 编辑收款方信息对话框 -->
<el-dialog :title="isEdit ? '编辑收款方信息' : '新增收款方信息'" :visible.sync="dialogVisible" width="550px" append-to-body>
<el-form :model="form" :rules="rules" ref="payeeFormRef" label-width="150px">
<el-form-item label="关联公司" prop="companyId">
<el-select v-model="form.companyId" placeholder="请选择关联公司" style="width: 100%;">
<el-option
v-for="item in companyOptions"
:key="item.id"
:label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="收款方单位名称" prop="payeeNameUnitName">
<el-input v-model="form.payeeNameUnitName" placeholder="请输入收款方单位名称" />
</el-form-item>
<el-form-item label="收款人" prop="payeeName">
<el-input v-model="form.payeeName" placeholder="请输入收款人" />
</el-form-item>
<el-form-item label="地址" prop="addr">
<el-input v-model="form.addr" placeholder="请输入地址" />
</el-form-item>
<el-form-item label="电话" prop="contTel">
<el-input v-model="form.contTel" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="应用楼宇" prop="buildingIds">
<el-popover
placement="bottom-start"
width="400"
trigger="click"
v-model="buildingPopoverVisible">
<el-tree
ref="buildingTree"
:data="buildingTreeData"
node-key="id"
:props="buildingProps"
show-checkbox
:default-expanded-keys="['parent_node']"
@check="handleBuildingCheck">
<span slot-scope="{ node, data }" class="custom-tree-node">
<i v-if="data.icon" :class="data.icon"></i>
<span>{{ node.label }}</span>
</span>
</el-tree>
<el-input
slot="reference"
v-model="form.buildingNames"
placeholder="请选择应用楼宇"
readonly
suffix-icon="el-icon-arrow-down">
</el-input>
</el-popover>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</div>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog title="提示" :visible.sync="deleteDialogVisible" width="360px" append-to-body>
<div class="delete-confirm">
<i class="el-icon-warning"></i>
<span>确定要删除该收款方信息吗</span>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="deleteDialogVisible = false"> </el-button>
<el-button type="primary" @click="confirmDelete"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getPayeeInfo, updatePayeeInfo, addPayeeInfo, deletePayeeInfo } from '@/api/finance'
import { getBuildingList } from '@/api/building'
import { API_SUCCESS_CODE } from '@/utils/constants'
export default {
name: 'PayeeInfo',
data() {
return {
//
queryParams: {
current: 1,
size: 10,
payeeNameUnitName: ''
},
//
loading: false,
//
payeeList: [],
total: 0,
//
payeeInfo: {
companyId: '',
payeeNameUnitName: '',
payeeName: '',
addr: '',
contTel: '',
buildingIds: []
},
//
buildingOptions: [],
//
companyOptions: [],
//
buildingTreeData: [],
buildingPopoverVisible: false,
buildingProps: {
label: 'label',
children: 'children'
},
//
dialogVisible: false,
deleteDialogVisible: false,
//
isEdit: false,
// ID
currentId: null,
//
form: {
companyId: '',
payeeNameUnitName: '',
payeeName: '',
addr: '',
contTel: '',
buildingIds: [],
buildingNames: ''
},
//
rules: {
companyId: [
{ required: true, message: '请选择关联公司', trigger: 'change' }
],
payeeNameUnitName: [
{ required: true, message: '请输入收款方单位名称', trigger: 'blur' }
],
payeeName: [
{ required: true, message: '请输入收款人', trigger: 'blur' }
],
contTel: [
{ pattern: /^(\d{3,4}-\d{7,8}(-\d{1,4})?|1[3-9]\d{9})$/, message: '电话格式不正确', trigger: 'blur' }
],
buildingIds: [
{ required: true, message: '请选择应用楼宇', trigger: 'change' }
]
}
}
},
created() {
this.getPayeeList()
this.getBuildingOptions()
this.getCompanyOptions()
},
methods: {
//
getPayeeList() {
this.loading = true
getPayeeInfo(this.queryParams).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.payeeList = res.data.data || []
this.total = res.data.total || 0
} else {
this.$message.error(res.message || '获取收款方信息列表失败')
//
this.payeeList = [{
id: 1,
companyId: 'COM001',
payeeNameUnitName: '智慧园区管理有限公司',
payeeName: '张三',
addr: '北京市海淀区中关村大街1号',
contTel: '13800138000',
buildingIds: 'BLD001,BLD002'
}]
this.total = 1
}
this.loading = false
}).catch(error => {
console.error('获取收款方信息列表失败', error)
this.$message.error('获取收款方信息列表失败')
//
this.payeeList = [{
id: 1,
companyId: 'COM001',
payeeNameUnitName: '智慧园区管理有限公司',
payeeName: '张三',
addr: '北京市海淀区中关村大街1号',
contTel: '13800138000',
buildingIds: 'BLD001,BLD002'
}]
this.total = 1
this.loading = false
})
},
//
getPayeeDetail(id) {
getPayeeInfo(id).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.payeeInfo = res.data || {}
} else {
this.$message.error(res.message || '获取收款方信息详情失败')
}
}).catch(error => {
console.error('获取收款方信息详情失败', error)
this.$message.error('获取收款方信息详情失败')
})
},
//
getBuildingOptions() {
getBuildingList().then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.buildingOptions = res.data || []
this.generateBuildingTree()
} else {
//
this.buildingOptions = [
{ id: 'BLD001', name: 'A栋办公楼' },
{ id: 'BLD002', name: 'B栋办公楼' },
{ id: 'BLD003', name: 'C栋研发中心' },
{ id: 'BLD004', name: 'D栋会议中心' },
{ id: 'BLD005', name: 'E栋商业配套' }
]
this.generateBuildingTree()
}
}).catch(error => {
console.error('获取楼宇列表失败', error)
//
this.buildingOptions = [
{ id: 'BLD001', name: 'A栋办公楼' },
{ id: 'BLD002', name: 'B栋办公楼' },
{ id: 'BLD003', name: 'C栋研发中心' },
{ id: 'BLD004', name: 'D栋会议中心' },
{ id: 'BLD005', name: 'E栋商业配套' }
]
this.generateBuildingTree()
})
},
//
generateBuildingTree() {
//
const parentNode = {
id: 'parent_node',
label: '小红马演示园区3',
children: this.buildingOptions.map(item => ({
id: item.id,
label: item.name,
icon: 'el-icon-office-building'
}))
}
this.buildingTreeData = [parentNode]
},
//
handleBuildingCheck() {
//
const checkedNodes = this.$refs.buildingTree.getCheckedNodes(false, false)
const leafNodes = checkedNodes.filter(node => !node.children || node.children.length === 0)
//
const selectedBuildings = leafNodes.filter(node => node.id !== 'parent_node')
// ID
this.form.buildingIds = selectedBuildings.map(node => node.id)
this.form.buildingNames = selectedBuildings.map(node => node.label).join('')
},
//
getCompanyOptions() {
//
this.companyOptions = [
{ id: 'COM001', name: '智慧园区管理有限公司' },
{ id: 'COM002', name: '科技创新服务有限公司' },
{ id: 'COM003', name: '数字产业发展有限公司' },
{ id: 'COM004', name: '绿色能源科技有限公司' },
{ id: 'COM005', name: '城市更新建设有限公司' }
]
},
//
handleSearch() {
this.queryParams.current = 1
this.getPayeeList()
},
//
resetQuery() {
this.queryParams = {
current: 1,
size: 10,
payeeNameUnitName: ''
}
this.getPayeeList()
},
//
handleSizeChange(val) {
this.queryParams.size = val
this.getPayeeList()
},
//
handleCurrentChange(val) {
this.queryParams.current = val
this.getPayeeList()
},
//
handleAdd() {
this.isEdit = false
this.currentId = null
this.form = {
companyId: '',
payeeNameUnitName: '',
payeeName: '',
addr: '',
contTel: '',
buildingIds: [],
buildingNames: ''
}
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.payeeFormRef) {
this.$refs.payeeFormRef.clearValidate()
}
//
if (this.$refs.buildingTree) {
this.$refs.buildingTree.setCheckedKeys([])
}
})
},
//
handleEdit(row) {
this.isEdit = true
this.currentId = row.id
// buildingIds
let buildingIds = row.buildingIds
if (typeof buildingIds === 'string') {
buildingIds = buildingIds.split(',').filter(id => id)
} else if (!Array.isArray(buildingIds)) {
buildingIds = []
}
//
let buildingNames = ''
if (Array.isArray(buildingIds) && buildingIds.length > 0) {
const selectedBuildingNames = []
this.buildingTreeData.forEach(parent => {
if (parent.children) {
parent.children.forEach(building => {
if (buildingIds.includes(building.id)) {
selectedBuildingNames.push(building.label)
}
})
}
})
buildingNames = selectedBuildingNames.join('')
}
this.form = {
...row,
buildingIds: buildingIds,
buildingNames: buildingNames
}
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.payeeFormRef) {
this.$refs.payeeFormRef.clearValidate()
}
//
if (this.$refs.buildingTree) {
this.$refs.buildingTree.setCheckedKeys(buildingIds)
}
})
},
//
submitForm() {
this.$refs.payeeFormRef.validate(valid => {
if (valid) {
// buildingIds
const formData = { ...this.form }
if (Array.isArray(formData.buildingIds)) {
formData.buildingIds = formData.buildingIds.join(',')
}
if (this.isEdit) {
//
updatePayeeInfo(this.currentId, formData).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('更新收款方信息成功')
this.dialogVisible = false
this.getPayeeList()
} else {
this.$message.error(res.message || '更新收款方信息失败')
}
}).catch(error => {
console.error('更新收款方信息失败', error)
this.$message.error('更新收款方信息失败')
})
} else {
//
addPayeeInfo(formData).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('新增收款方信息成功')
this.dialogVisible = false
this.getPayeeList()
} else {
this.$message.error(res.message || '新增收款方信息失败')
}
}).catch(error => {
console.error('新增收款方信息失败', error)
this.$message.error('新增收款方信息失败')
})
}
}
})
},
//
handleDelete(row) {
this.currentId = row.id
this.deleteDialogVisible = true
},
//
confirmDelete() {
if (!this.currentId) return
deletePayeeInfo(this.currentId).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('删除成功')
this.getPayeeList()
} else {
this.$message.error(res.message || '删除失败')
}
this.deleteDialogVisible = false
}).catch(error => {
console.error('删除失败', error)
this.$message.error('删除失败')
this.deleteDialogVisible = false
})
},
//
formatCompanyName(companyId) {
if (!companyId) return '-'
const company = this.companyOptions.find(item => item.id === companyId)
return company ? company.name : companyId
},
//
formatBuildingNames(buildingIds) {
if (!buildingIds) return '-'
// buildingIds
let buildingIdArray = buildingIds
if (typeof buildingIds === 'string') {
buildingIdArray = buildingIds.split(',').filter(id => id)
}
if (!Array.isArray(buildingIdArray) || buildingIdArray.length === 0) {
return '-'
}
const buildingNames = []
this.buildingTreeData.forEach(parent => {
if (parent.children) {
parent.children.forEach(building => {
if (buildingIdArray.includes(building.id)) {
buildingNames.push(building.label)
}
})
}
})
return buildingNames.length > 0 ? buildingNames.join('') : '-'
}
}
}
</script>
<style lang="scss" scoped>
.payee-info-container {
.header-actions,
.filter-container {
margin-bottom: 15px;
}
.info-card {
margin-bottom: 20px;
.card-header {
font-weight: bold;
}
.info-content {
padding: 10px 0;
}
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
.delete-confirm {
display: flex;
align-items: center;
i {
font-size: 24px;
color: #e6a23c;
margin-right: 10px;
}
}
}
</style>

View File

@ -0,0 +1,940 @@
<template>
<div class="receipt-template-container">
<!-- 搜索栏 -->
<div class="filter-container">
<el-input v-model="queryParams.templateName" placeholder="请输入模板名称" clearable style="width: 250px;" class="filter-item" />
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button style="float: right;" icon="el-icon-plus" type="primary" @click="handleUploadTemplate">新增收据模版</el-button>
<el-button style="float: right;" @click="handleDownloadExample">下载样例模板</el-button>
</div>
<!-- 模板列表 -->
<el-table v-loading="loading" :data="templateList" border style="margin-top: 15px;">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="templateName" label="模板名称" />
<el-table-column label="应用楼宇">
<template slot-scope="scope">
{{ formatBuildingNames(scope.row.buildingIds) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="250">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handlePreviewTemplate(scope.row)">预览</el-button>
<el-button type="text" size="small" @click="handleDownloadTemplate(scope.row)">下载</el-button>
<el-button type="text" size="small" @click="handleEditTemplate(scope.row)">编辑</el-button>
<el-button type="text" size="small" @click="handleDeleteTemplate(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="queryParams.current" :page-sizes="[10, 20, 30, 50]" :page-size="queryParams.size"
layout="total, sizes, prev, pager, next, jumper" :total="total" />
</div>
<!-- 上传/编辑模板对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px" append-to-body>
<el-tabs v-model="activeTab">
<el-tab-pane label="复制关键字" name="keywords">
<div class="keywords-container">
<div class="keyword-section">
<h4>收据信息</h4>
<div class="keyword-list">
<div v-for="(item, index) in keywords.receiptInfo" :key="'receipt-'+index" class="keyword-item" @click="copyKeyword(item)">
{{ item }}
</div>
</div>
</div>
<div class="keyword-section">
<h4>交收款方</h4>
<div class="keyword-list">
<div v-for="(item, index) in keywords.payerInfo" :key="'payer-'+index" class="keyword-item" @click="copyKeyword(item)">
{{ item }}
</div>
</div>
</div>
<div class="keyword-section">
<h4>房源信息</h4>
<div class="keyword-list">
<div v-for="(item, index) in keywords.propertyInfo" :key="'property-'+index" class="keyword-item" @click="copyKeyword(item)">
{{ item }}
</div>
</div>
</div>
<div class="keyword-section">
<h4>账单信息</h4>
<div class="keyword-list">
<div v-for="(item, index) in keywords.billInfo" :key="'bill-'+index" class="keyword-item" @click="copyKeyword(item)">
{{ item }}
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="上传收据打印模板" name="upload">
<el-form :model="templateForm" :rules="rules" ref="templateFormRef" label-width="120px">
<el-form-item label="模板名称" prop="templateName">
<el-input v-model="templateForm.templateName" placeholder="请输入模板名称" />
</el-form-item>
<el-form-item label="模板文件" prop="file" v-if="!isEdit || uploadNewFile">
<el-upload
:action="'#'"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:file-list="fileList"
accept=".docx">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">仅支持docx格式大小不超过5M</div>
</el-upload>
</el-form-item>
<el-form-item v-if="isEdit && !uploadNewFile">
<el-checkbox v-model="uploadNewFile">更新模板文件</el-checkbox>
</el-form-item>
<el-form-item label="应用楼宇" prop="buildingIds">
<el-popover
placement="bottom-start"
width="400"
trigger="click"
v-model="buildingPopoverVisible">
<el-tree
ref="buildingTree"
:data="buildingTreeData"
node-key="id"
:props="buildingProps"
show-checkbox
:default-expanded-keys="['parent_node']"
@check="handleBuildingCheck">
<span slot-scope="{ node, data }" class="custom-tree-node">
<i v-if="data.icon" :class="data.icon"></i>
<span>{{ node.label }}</span>
</span>
</el-tree>
<el-input
slot="reference"
v-model="templateForm.buildingNames"
placeholder="请选择应用楼宇"
readonly
suffix-icon="el-icon-arrow-down">
</el-input>
</el-popover>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitTemplateForm" v-if="activeTab === 'upload'"> </el-button>
</div>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog title="提示" :visible.sync="deleteDialogVisible" width="360px" append-to-body>
<div class="delete-confirm">
<i class="el-icon-warning"></i>
<span>确定要删除该收据模板吗</span>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="deleteDialogVisible = false"> </el-button>
<el-button type="primary" @click="confirmDelete"> </el-button>
</div>
</el-dialog>
<!-- 预览模板对话框 -->
<el-dialog title="收据模板预览" :visible.sync="previewDialogVisible" width="800px" append-to-body>
<div class="preview-container" v-loading="previewLoading">
<div v-if="previewUrl" class="preview-iframe">
<iframe :src="previewUrl" width="100%" height="500px" frameborder="0"></iframe>
</div>
<div v-else-if="!previewLoading" class="preview-error">
<i class="el-icon-warning"></i>
<p>预览加载失败请尝试下载后查看</p>
<div class="preview-actions">
<el-button type="primary" @click="downloadCurrentPreview">下载文档</el-button>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="previewDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getReceiptTemplateList,
addReceiptTemplate,
updateReceiptTemplate,
deleteReceiptTemplate,
previewReceiptTemplate,
downloadReceiptTemplate,
downloadExampleTemplate,
getReceiptKeywords,
checkTemplateName
} from '@/api/finance'
import { getBuildingList } from '@/api/building'
import { API_SUCCESS_CODE } from '@/utils/constants'
export default {
name: 'ReceiptTemplate',
data() {
//
const validateTemplateName = (rule, value, callback) => {
if (!value) {
return callback(new Error('请输入模板名称'))
}
//
if (this.isEdit && value === this.originalName) {
return callback()
}
checkTemplateName({
templateName: value,
id: this.isEdit ? this.currentId : undefined
}).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
if (res.data) {
callback(new Error('模板名称已存在'))
} else {
callback()
}
} else {
callback()
}
}).catch(() => {
callback()
})
}
//
const validateFile = (rule, value, callback) => {
//
if (this.isEdit && !this.uploadNewFile) {
return callback()
}
if (this.fileList.length === 0) {
return callback(new Error('请上传模板文件'))
}
callback()
}
return {
//
queryParams: {
current: 1,
size: 10,
templateName: ''
},
// tab
activeTab: 'upload',
// 使
originalName: '',
//
loading: false,
//
templateList: [],
total: 0,
//
buildingOptions: [],
//
buildingTreeData: [],
buildingPopoverVisible: false,
buildingProps: {
label: 'label',
children: 'children'
},
//
fileList: [],
//
dialogVisible: false,
deleteDialogVisible: false,
previewDialogVisible: false,
//
isEdit: false,
uploadNewFile: false,
// ID
currentId: null,
//
templateForm: {
templateName: '',
buildingIds: [],
buildingNames: ''
},
//
rules: {
templateName: [
{ required: true, message: '请输入模板名称', trigger: 'blur' },
{ validator: validateTemplateName, trigger: 'blur' }
],
file: [
{ validator: validateFile, trigger: 'change' }
],
buildingIds: [
{ required: true, message: '请选择应用楼宇', trigger: 'change' }
]
},
//
keywords: {
receiptInfo: ['${收据编号}', '${汇款方式}', '${费用类型}', '${费用名称}', '${开据金额}', '${开据金额(大写)}', '${开据时间}', '${开据人}', '${租赁面积}'],
payerInfo: ['${交款单位}', '${交款人}', '${交款方电话}', '${交款方地址}', '${收款单位}', '${收款人}', '${收款方电话}', '${收款方地址}'],
propertyInfo: ['${项目名称}', '${项目地址}', '${楼宇名称}', '${楼层房号}'],
billInfo: ['${账单编号}', '${楼宇名称}']
},
//
previewBlob: null,
currentPreviewName: '',
previewLoading: false,
previewUrl: '', // PDFURL
}
},
computed: {
dialogTitle() {
return this.isEdit ? '编辑收据模板' : '新增收据模板'
}
},
created() {
this.getTemplateList()
this.getBuildingOptions()
this.fetchKeywords()
},
methods: {
//
getTemplateList() {
this.loading = true
getReceiptTemplateList(this.queryParams).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.templateList = res.data.data || []
this.total = res.data.total || 0
} else {
this.$message.error(res.message || '获取收据模板列表失败')
this.templateList = []
this.total = 0
}
this.loading = false
}).catch(error => {
console.error('获取收据模板列表失败', error)
this.$message.error('获取收据模板列表失败')
this.templateList = []
this.total = 0
this.loading = false
})
},
//
generateBuildingTree() {
//
const parentNode = {
id: 'parent_node',
label: '小红马演示园区3',
children: this.buildingOptions.map(item => ({
id: item.id,
label: item.name,
icon: 'el-icon-office-building'
}))
}
this.buildingTreeData = [parentNode]
},
//
handleBuildingCheck() {
//
const checkedNodes = this.$refs.buildingTree.getCheckedNodes(false, false)
const leafNodes = checkedNodes.filter(node => !node.children || node.children.length === 0)
//
const selectedBuildings = leafNodes.filter(node => node.id !== 'parent_node')
// ID
this.templateForm.buildingIds = selectedBuildings.map(node => node.id)
this.templateForm.buildingNames = selectedBuildings.map(node => node.label).join('')
},
//
getBuildingOptions() {
getBuildingList().then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.buildingOptions = res.data || []
this.generateBuildingTree()
} else {
//
this.buildingOptions = [
{ id: 'BLD001', name: 'A栋办公楼' },
{ id: 'BLD002', name: 'B栋办公楼' },
{ id: 'BLD003', name: 'C栋研发中心' },
{ id: 'BLD004', name: 'D栋会议中心' },
{ id: 'BLD005', name: 'E栋商业配套' }
]
this.generateBuildingTree()
}
}).catch(error => {
console.error('获取楼宇列表失败', error)
//
this.buildingOptions = [
{ id: 'BLD001', name: 'A栋办公楼' },
{ id: 'BLD002', name: 'B栋办公楼' },
{ id: 'BLD003', name: 'C栋研发中心' },
{ id: 'BLD004', name: 'D栋会议中心' },
{ id: 'BLD005', name: 'E栋商业配套' }
]
this.generateBuildingTree()
})
},
//
handleSearch() {
this.queryParams.current = 1
this.getTemplateList()
},
//
resetQuery() {
this.queryParams = {
current: 1,
size: 10,
templateName: ''
}
this.getTemplateList()
},
//
handleSizeChange(val) {
this.queryParams.size = val
this.getTemplateList()
},
//
handleCurrentChange(val) {
this.queryParams.current = val
this.getTemplateList()
},
//
handleFileChange(file) {
//
const isDocx = file.raw.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
if (!isDocx) {
this.$message.error('请上传docx格式的文件')
this.fileList = []
return false
}
//
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
this.$message.error('文件大小不能超过5MB')
this.fileList = []
return false
}
this.fileList = [file]
},
//
handleFileRemove() {
this.fileList = []
},
//
handleUploadTemplate() {
this.isEdit = false
this.uploadNewFile = true
this.currentId = null
this.originalName = ''
this.templateForm = {
templateName: '',
buildingIds: [],
buildingNames: ''
}
this.fileList = []
this.activeTab = 'upload'
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.templateFormRef) {
this.$refs.templateFormRef.clearValidate()
}
//
if (this.$refs.buildingTree) {
this.$refs.buildingTree.setCheckedKeys([])
}
})
},
//
handleEditTemplate(row) {
this.isEdit = true
this.uploadNewFile = false
this.currentId = row.id
this.originalName = row.templateName
// buildingIds
let buildingIds = row.buildingIds
if (typeof buildingIds === 'string') {
buildingIds = buildingIds.split(',').filter(id => id)
} else if (!Array.isArray(buildingIds)) {
buildingIds = []
}
//
let buildingNames = ''
if (Array.isArray(buildingIds) && buildingIds.length > 0) {
const selectedBuildingNames = []
this.buildingTreeData.forEach(parent => {
if (parent.children) {
parent.children.forEach(building => {
if (buildingIds.includes(building.id)) {
selectedBuildingNames.push(building.label)
}
})
}
})
buildingNames = selectedBuildingNames.join('')
}
this.templateForm = {
templateName: row.templateName,
buildingIds: buildingIds,
buildingNames: buildingNames
}
this.fileList = []
this.activeTab = 'upload'
this.dialogVisible = true
this.$nextTick(() => {
if (this.$refs.templateFormRef) {
this.$refs.templateFormRef.clearValidate()
}
//
if (this.$refs.buildingTree) {
this.$refs.buildingTree.setCheckedKeys(buildingIds)
}
})
},
//
submitTemplateForm() {
this.$refs.templateFormRef.validate(valid => {
if (valid) {
// FormData
const formData = new FormData()
formData.append('templateName', this.templateForm.templateName)
// buildingIds
if (Array.isArray(this.templateForm.buildingIds)) {
this.templateForm.buildingIds.forEach(id => {
formData.append('buildingIds', id)
})
}
//
if (!this.isEdit || this.uploadNewFile) {
formData.append('file', this.fileList[0].raw)
}
if (this.isEdit) {
//
updateReceiptTemplate(this.currentId, formData).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('更新收据模板成功')
this.dialogVisible = false
this.getTemplateList()
} else {
this.$message.error(res.message || '更新收据模板失败')
}
}).catch(error => {
console.error('更新收据模板失败', error)
this.$message.error('更新收据模板失败')
})
} else {
//
addReceiptTemplate(formData).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('新增收据模板成功')
this.dialogVisible = false
this.getTemplateList()
} else {
this.$message.error(res.message || '新增收据模板失败')
}
}).catch(error => {
console.error('新增收据模板失败', error)
this.$message.error('新增收据模板失败')
})
}
}
})
},
//
async handlePreviewTemplate(row) {
this.previewLoading = true
this.previewDialogVisible = true
this.currentPreviewName = row.templateName || '收据模板'
this.previewUrl = ''
try {
// APIPDF
const response = await previewReceiptTemplate(row.id)
// - PDF
const blob = new Blob([response.data], { type: 'application/pdf' })
this.previewBlob = blob
// URL
this.previewUrl = URL.createObjectURL(blob)
this.previewLoading = false
} catch (error) {
console.error('预览失败', error)
this.$message.error('获取预览数据失败')
this.previewLoading = false
}
},
//
downloadCurrentPreview() {
if (!this.previewBlob) {
this.$message.error('没有可下载的文档')
return
}
const url = URL.createObjectURL(this.previewBlob)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `${this.currentPreviewName}.pdf`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
//
handleDownloadTemplate(row) {
downloadReceiptTemplate(row.id).then(response => {
//
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `${row.templateName}.docx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}).catch(error => {
console.error('下载模板失败', error)
this.$message.error('下载模板失败')
})
},
//
handleDownloadExample() {
downloadExampleTemplate().then(response => {
//
const url = window.URL.createObjectURL(new Blob([response.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', '收据模板样例.docx')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}).catch(error => {
console.error('下载样例模板失败', error)
this.$message.error('下载样例模板失败')
})
},
//
handleDeleteTemplate(row) {
this.currentId = row.id
this.deleteDialogVisible = true
},
//
confirmDelete() {
if (!this.currentId) return
deleteReceiptTemplate(this.currentId).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('删除成功')
this.getTemplateList()
} else {
this.$message.error(res.message || '删除失败')
}
this.deleteDialogVisible = false
}).catch(error => {
console.error('删除失败', error)
this.$message.error('删除失败')
this.deleteDialogVisible = false
})
},
//
fetchKeywords() {
getReceiptKeywords().then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.keywords = res.data
} else {
this.$message.error(res.message || '获取关键字失败')
}
}).catch(error => {
console.error('获取关键字失败', error)
this.$message.error('获取关键字失败')
})
},
//
copyKeyword(keyword) {
const textarea = document.createElement('textarea')
textarea.value = keyword
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
this.$message.success('已复制到剪贴板')
},
//
handleShowKeywords() {
//
this.fetchKeywords()
//
this.isEdit = false
this.uploadNewFile = true
this.currentId = null
this.originalName = ''
this.templateForm = {
templateName: '',
buildingIds: [],
buildingNames: ''
}
this.fileList = []
// active tab
this.activeTab = 'keywords'
this.dialogVisible = true
},
//
formatBuildingNames(buildingIds) {
if (!buildingIds) return '-'
// buildingIds
let buildingIdArray = buildingIds
if (typeof buildingIds === 'string') {
buildingIdArray = buildingIds.split(',').filter(id => id)
}
if (!Array.isArray(buildingIdArray) || buildingIdArray.length === 0) {
return '-'
}
const buildingNames = []
this.buildingTreeData.forEach(parent => {
if (parent.children) {
parent.children.forEach(building => {
if (buildingIdArray.includes(building.id)) {
buildingNames.push(building.label)
}
})
}
})
return buildingNames.length > 0 ? buildingNames.join('') : '-'
}
}
}
</script>
<style lang="scss" scoped>
.receipt-template-container {
.action-bar,
.filter-container {
margin-bottom: 15px;
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
.delete-confirm {
display: flex;
align-items: center;
i {
font-size: 24px;
color: #e6a23c;
margin-right: 10px;
}
}
}
.keywords-container {
padding: 15px;
.keyword-section {
margin-bottom: 20px;
h4 {
margin: 0 0 10px 0;
font-weight: bold;
padding-bottom: 8px;
border-bottom: 1px solid #EBEEF5;
}
.keyword-list {
display: flex;
flex-wrap: wrap;
.keyword-item {
margin: 5px;
padding: 8px 15px;
background-color: #f5f7fa;
border: 1px solid #DCDFE6;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
&:hover {
background-color: #ecf5ff;
color: #409EFF;
border-color: #c6e2ff;
}
}
}
}
}
/* 深度选择器 */
:deep(.el-tree-node__content) {
height: 32px;
line-height: 32px;
}
:deep(.el-tree-node__label) {
font-size: 14px;
}
:deep(.custom-tree-node) {
flex: 1;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
:deep(.el-popover) {
padding: 10px;
.el-tree {
max-height: 300px;
overflow-y: auto;
}
}
:deep(.el-form-item__content .el-input) {
width: 100%;
}
.preview-container {
min-height: 500px;
max-height: 700px;
overflow: auto;
.preview-content {
width: 100%;
padding: 20px;
background-color: #fff;
p, h1, h2, h3, h4, h5, h6, table, ul, ol {
margin-bottom: 12px;
}
table {
border-collapse: collapse;
width: 100%;
th, td {
border: 1px solid #ddd;
padding: 8px;
}
th {
background-color: #f2f2f2;
text-align: left;
}
}
}
.preview-iframe {
width: 100%;
height: 500px;
border: none;
}
.preview-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
color: #909399;
i {
font-size: 48px;
margin-bottom: 20px;
}
p {
font-size: 16px;
}
.preview-actions {
margin-top: 20px;
}
}
}
.preview-mode-switch {
float: left;
margin-right: 20px;
}
</style>

View File

@ -0,0 +1,474 @@
<template>
<div class="receipt-setting-container">
<el-tabs v-model="activeTab">
<el-tab-pane label="收据编号" name="receiptNum">
<!-- 收据编号规则查询 -->
<div class="filter-container">
<el-input v-model="queryParams.ruleName" placeholder="请输入收据编号规则名称" clearable style="width: 250px;"
class="filter-item" />
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" icon="el-icon-plus" style="float: right;" @click="handleAddRule">新增收据编号规则</el-button>
</div>
<!-- 收据编号规则列表 -->
<el-table v-loading="loading" :data="ruleList" border>
<el-table-column prop="ruleName" label="收据编号规则名称" />
<el-table-column prop="prefix" label="收据编号前缀" />
<el-table-column prop="startNumber" label="开始编号" />
<el-table-column prop="endNumber" label="结束编号" />
<el-table-column prop="currentNumber" label="当前编号" />
<el-table-column label="应用项目">
<template slot-scope="scope">
{{ formatProjectNames(scope.row.projectList) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleDeleteRule(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="queryParams.current" :page-sizes="[10, 20, 30, 50]" :page-size="queryParams.size"
layout="total, sizes, prev, pager, next, jumper" :total="total" />
</div>
</el-tab-pane>
<el-tab-pane label="收款方信息" name="payeeInfo">
<payee-info />
</el-tab-pane>
<el-tab-pane label="收据模板" name="receiptTemplate">
<receipt-template />
</el-tab-pane>
</el-tabs>
<!-- 新增收据编号规则对话框 -->
<el-dialog title="新增收据编号规则" :visible.sync="ruleDialogVisible" width="500px" append-to-body>
<el-form :model="ruleForm" :rules="ruleRules" ref="ruleFormRef" label-width="120px">
<el-form-item label="收据编号名称" prop="ruleName">
<el-input v-model="ruleForm.ruleName" placeholder="请输入收据编号名称" />
</el-form-item>
<el-form-item label="收据编号前缀" prop="prefix">
<el-input v-model="ruleForm.prefix" placeholder="请输入收据编号前缀" />
</el-form-item>
<el-form-item label="开始编号" prop="startNumber">
<el-input v-model="ruleForm.startNumber" placeholder="请输入开始编号" type="number" />
</el-form-item>
<el-form-item label="结束编号" prop="endNumber">
<el-input v-model="ruleForm.endNumber" placeholder="请输入结束编号" type="number" />
</el-form-item>
<el-form-item label="应用项目" prop="projectIds">
<el-popover
placement="bottom-start"
width="400"
trigger="click"
v-model="projectPopoverVisible">
<el-tree
ref="projectTree"
:data="projectTreeData"
node-key="id"
:props="projectProps"
show-checkbox
:default-expanded-keys="['parent_node']"
@check="handleProjectCheck">
<span slot-scope="{ node, data }" class="custom-tree-node">
<i v-if="data.icon" :class="data.icon"></i>
<span>{{ node.label }}</span>
</span>
</el-tree>
<el-input
slot="reference"
v-model="ruleForm.projectNames"
placeholder="请选择应用项目"
readonly
suffix-icon="el-icon-arrow-down">
</el-input>
</el-popover>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="ruleDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitRuleForm"> </el-button>
</div>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog title="提示" :visible.sync="deleteDialogVisible" width="360px" append-to-body>
<div class="delete-confirm">
<i class="el-icon-warning"></i>
<span>确定要删除该收据编号规则吗</span>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="deleteDialogVisible = false"> </el-button>
<el-button type="primary" @click="confirmDelete"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listReceiptRule,
addReceiptRule,
deleteReceiptRule,
getProjectList
} from '@/api/finance'
import { API_SUCCESS_CODE } from '@/utils/constants'
import PayeeInfo from './PayeeInfo.vue'
import ReceiptTemplate from './ReceiptTemplate.vue'
export default {
name: 'ReceiptSetting',
components: {
PayeeInfo,
ReceiptTemplate
},
data() {
//
const validateEndNum = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入结束编号'))
} else if (!/^\d+$/.test(value)) {
callback(new Error('结束编号必须为数字'))
} else if (parseInt(value) <= parseInt(this.ruleForm.startNumber)) {
callback(new Error('结束编号必须大于开始编号'))
} else if (value.length !== this.ruleForm.startNumber.length) {
callback(new Error('结束编号和开始编号的位数必须相同'))
} else {
callback()
}
}
return {
//
activeTab: 'receiptNum',
//
queryParams: {
current: 1,
size: 10,
ruleName: '',
},
//
loading: false,
//
ruleList: [],
total: 0,
//
projectOptions: [],
//
projectTreeData: [],
projectPopoverVisible: false,
projectProps: {
label: 'label',
children: 'children'
},
//
ruleDialogVisible: false,
deleteDialogVisible: false,
// ID
currentRuleId: null,
//
ruleForm: {
ruleName: '',
prefix: '',
startNumber: '',
endNumber: '',
projectIds: [],
projectNames: ''
},
//
ruleRules: {
ruleName: [
{ required: true, message: '请输入收据编号名称', trigger: 'blur' }
],
startNumber: [
{ required: true, message: '请输入开始编号', trigger: 'blur' },
{ pattern: /^\d+$/, message: '开始编号必须为数字', trigger: 'blur' }
],
endNumber: [
{ required: true, validator: validateEndNum, trigger: 'blur' }
],
projectIds: [
{ required: true, message: '请选择应用项目', trigger: 'change' }
]
},
}
},
created() {
this.getRuleList()
this.getProjects()
},
methods: {
//
getRuleList() {
this.loading = true
listReceiptRule(this.queryParams).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.ruleList = res.data.data || []
this.total = res.data.total || 0
} else {
this.$message.error(res.message || '获取收据编号规则列表失败')
this.ruleList = []
this.total = 0
}
this.loading = false
}).catch(error => {
console.error('获取收据编号规则列表失败', error)
this.$message.error('获取收据编号规则列表失败')
this.ruleList = []
this.total = 0
this.loading = false
})
},
//
getProjects() {
getProjectList().then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.projectOptions = res.data || []
this.generateProjectTree()
} else {
//
this.projectOptions = [
{ id: '2', name: '高新科技园区' },
{ id: '3', name: '软件产业园' },
{ id: '4', name: '文创产业园' },
{ id: '5', name: '生物医药园区' },
{ id: '8', name: '智能制造基地' }
]
this.generateProjectTree()
}
}).catch(error => {
console.error('获取项目列表失败', error)
//
this.projectOptions = [
{ id: '2', name: '高新科技园区' },
{ id: '3', name: '软件产业园' },
{ id: '4', name: '文创产业园' },
{ id: '5', name: '生物医药园区' },
{ id: '8', name: '智能制造基地' }
]
this.generateProjectTree()
})
},
//
generateProjectTree() {
//
// "3"
const parentNode = {
id: 'parent_node',
label: '小红马演示园区3',
children: this.projectOptions.map(item => ({
id: item.id,
label: item.name,
icon: 'el-icon-office-building'
}))
}
this.projectTreeData = [parentNode]
},
//
handleProjectCheck() {
//
const checkedNodes = this.$refs.projectTree.getCheckedNodes(false, false)
const leafNodes = checkedNodes.filter(node => !node.children || node.children.length === 0)
//
const selectedProjects = leafNodes.filter(node => node.id !== 'parent_node')
// ID
this.ruleForm.projectIds = selectedProjects.map(node => node.id)
this.ruleForm.projectNames = selectedProjects.map(node => node.label).join('')
},
//
handleAddRule() {
this.ruleForm = {
ruleName: '',
prefix: '',
startNumber: '',
endNumber: '',
projectIds: [],
projectNames: ''
}
this.ruleDialogVisible = true
this.$nextTick(() => {
if (this.$refs.ruleFormRef) {
this.$refs.ruleFormRef.clearValidate()
}
//
if (this.$refs.projectTree) {
this.$refs.projectTree.setCheckedKeys([])
}
})
},
//
submitRuleForm() {
this.$refs.ruleFormRef.validate(valid => {
if (valid) {
// projectIds
const submitData = {
...this.ruleForm,
projectIds: Array.isArray(this.ruleForm.projectIds) ?
this.ruleForm.projectIds.join(',') :
this.ruleForm.projectIds
}
addReceiptRule(submitData).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('新增收据编号规则成功')
this.ruleDialogVisible = false
this.getRuleList()
} else {
this.$message.error(res.message || '新增收据编号规则失败')
}
}).catch(error => {
console.error('新增收据编号规则失败', error)
this.$message.error('新增收据编号规则失败')
})
}
})
},
//
handleDeleteRule(row) {
this.currentRuleId = row.id
this.deleteDialogVisible = true
},
//
confirmDelete() {
if (!this.currentRuleId) return
deleteReceiptRule(this.currentRuleId).then(res => {
if (res && res.code === API_SUCCESS_CODE) {
this.$message.success('删除成功')
this.getRuleList()
} else {
this.$message.error(res.message || '删除失败')
}
this.deleteDialogVisible = false
}).catch(error => {
console.error('删除失败', error)
this.$message.error('删除失败')
this.deleteDialogVisible = false
})
},
//
formatProjectNames(projectList) {
if (!projectList || !Array.isArray(projectList) || projectList.length === 0 ) {
return '-'
}
return projectList.map(item => item && item.projectName||'').join('')
},
//
handleSearch() {
this.queryParams.current = 1
this.getRuleList()
},
//
resetQuery() {
this.queryParams = {
current: 1,
size: 10,
ruleName: ''
}
this.getRuleList()
},
//
handleSizeChange(val) {
this.queryParams.size = val
this.getRuleList()
},
//
handleCurrentChange(val) {
this.queryParams.current = val
this.getRuleList()
},
}
}
</script>
<style lang="scss" scoped>
.receipt-setting-container {
padding: 20px;
.filter-container {
margin: 15px 0;
.filter-item {
margin-right: 10px;
}
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
.delete-confirm {
display: flex;
align-items: center;
i {
font-size: 24px;
color: #e6a23c;
margin-right: 10px;
}
}
:v-deep .el-tree-node__content {
height: 32px;
line-height: 32px;
}
:v-deep .el-tree-node__label {
font-size: 14px;
}
:v-deep .custom-tree-node {
flex: 1;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
:v-deep .el-popover {
padding: 10px;
.el-tree {
max-height: 300px;
overflow-y: auto;
}
}
:v-deep .el-form-item__content .el-input {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<div class="clue-management-container">
<h1>线索管理页面</h1>
<p>这里是线索管理的内容根据需求继续完善</p>
</div>
</template>
<script>
export default {
name: 'ClueManagement',
data() {
return {
}
},
created() {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.clue-management-container {
padding: 20px;
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<div class="intent-customer-container">
<h1>意向客户页面</h1>
<p>这里是意向客户的内容根据需求继续完善</p>
</div>
</template>
<script>
export default {
name: 'IntentCustomer',
data() {
return {
}
},
created() {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.intent-customer-container {
padding: 20px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,844 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 左侧分组区域 -->
<el-col :span="6">
<div class="group-container">
<div class="group-header">
<h3>分组管理</h3>
<el-button
type="primary"
size="mini"
icon="el-icon-plus"
@click="handleAddGroup">
新增
</el-button>
</div>
<div class="group-search">
<el-input
v-model="groupSearchInput"
placeholder="请输入分组名称"
clearable
size="small"
@keyup.enter.native="handleGroupSearch">
<el-button slot="append" icon="el-icon-search" @click="handleGroupSearch"></el-button>
</el-input>
</div>
<div class="group-menu">
<el-menu
:default-active="activeGroupId"
class="el-menu-vertical-demo"
@select="handleGroupSelect">
<el-menu-item
v-for="group in filteredGroupList"
:key="group.id"
:index="group.id.toString()"
class="group-item">
<span>{{ group.groupName }}</span>
<div class="group-actions">
<el-tooltip content="编辑" placement="top">
<i class="el-icon-edit" @click.stop="handleEditGroup(group)"></i>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<i class="el-icon-delete" @click.stop="handleDeleteGroup(group)"></i>
</el-tooltip>
</div>
</el-menu-item>
</el-menu>
</div>
</div>
</el-col>
<!-- 右侧标签区域 -->
<el-col :span="18">
<div class="tag-container">
<div class="tag-header">
<div class="header-title">
<h3>标签管理</h3>
<span v-if="activeGroup">- {{ activeGroup.groupName }}</span>
</div>
<el-button
type="primary"
icon="el-icon-plus"
@click="handleAddTag"
:disabled="!activeGroupId">
新增标签
</el-button>
</div>
<!-- 标签搜索区域 -->
<div class="tag-search">
<el-form :inline="true" :model="tagQueryParams" class="search-form">
<el-form-item label="标签名称">
<el-input
v-model="tagQueryParams.tagName"
placeholder="请输入标签名称"
clearable
@keyup.enter.native="handleTagQuery">
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleTagQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetTagQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 标签表格 -->
<el-table
v-loading="tableLoading"
:data="paginationTagList"
style="width: 100%"
border>
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="tagName"
label="标签名称">
</el-table-column>
<el-table-column
prop="intentionCustomerCount"
label="意向客户数量"
width="120">
</el-table-column>
<el-table-column
label="操作"
width="150">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
@click="handleEditTag(scope.row)">
<i class="el-icon-edit"></i> 编辑
</el-button>
<el-button
size="mini"
type="text"
class="delete-btn"
@click="handleDeleteTag(scope.row)">
<i class="el-icon-delete"></i> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-container">
<el-pagination
v-show="total > 0"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="tagQueryParams.pageNum"
:page-sizes="[10, 20, 30, 50]"
:page-size="tagQueryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</el-col>
</el-row>
<!-- 分组弹窗 -->
<el-dialog :title="groupDialogType === 'add' ? '新增分组' : '编辑分组'" :visible.sync="groupDialogVisible" width="500px" append-to-body>
<el-form :model="groupForm" :rules="groupRules" ref="groupForm" label-width="100px">
<el-form-item label="分组名称" prop="groupName">
<el-input v-model="groupForm.groupName" placeholder="请输入分组名称" style="width: 100%"></el-input>
</el-form-item>
<el-form-item v-if="groupDialogType === 'add'" label="标签名称" prop="tagNames">
<el-select
v-model="groupForm.tagNames"
multiple
allow-create
filterable
default-first-option
placeholder="请输入标签名称,按回车确认"
style="width: 100%">
</el-select>
<div class="el-form-item-tip">按回车键可输入多个标签名称</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="groupDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitGroupForm"> </el-button>
</div>
</el-dialog>
<!-- 标签弹窗 -->
<el-dialog :title="tagDialogType === 'add' ? '新增标签' : '编辑标签'" :visible.sync="tagDialogVisible" width="500px" append-to-body>
<el-form :model="tagForm" :rules="tagRules" ref="tagForm" label-width="100px">
<el-form-item label="选择分组" prop="groupId">
<el-select v-model="tagForm.groupId" placeholder="请选择分组" style="width: 100%">
<el-option
v-for="group in groupList"
:key="group.id"
:label="group.groupName"
:value="group.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="标签名称" prop="names">
<el-select
v-model="tagForm.names"
multiple
allow-create
filterable
default-first-option
placeholder="请输入标签名称,按回车确认"
style="width: 100%">
</el-select>
<div class="el-form-item-tip">按回车键可输入多个标签名称</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="tagDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitTagForm"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getTagGroupList,
getAllTagGroups,
getTagGroupDetail,
addTagGroup,
updateTagGroup,
deleteTagGroup,
checkTagGroupName,
getTagList,
getTagListByGroupId,
getTagDetail,
addTag,
updateTag,
deleteTag,
checkTagName
} from '@/api/merchant'
export default {
name: 'TagManagement',
data() {
//
const validateGroupName = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入分组名称'))
return
}
//
if (this.groupDialogType === 'edit' && this.groupForm.originalName === value) {
callback()
return
}
const params = {
groupName: value
}
if (this.groupDialogType === 'edit') {
params.id = this.groupForm.id
}
checkTagGroupName(params).then(res => {
if (res.success && res.data) {
callback()
} else {
callback(new Error('分组名称已存在'))
}
}).catch(() => {
callback(new Error('校验分组名称失败'))
})
}
//
const validateTagName = (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请输入至少一个标签名称'))
return
}
//
if (value.length > 1 || this.tagDialogType === 'add') {
callback()
return
}
//
if (this.tagDialogType === 'edit' && this.tagForm.originalName === value[0]) {
callback()
return
}
const params = {
groupId: this.tagForm.groupId,
tagName: value[0]
}
if (this.tagDialogType === 'edit') {
params.id = this.tagForm.id
}
checkTagName(params).then(res => {
if (res.success && res.data) {
callback()
} else {
callback(new Error('标签名称在该分组下已存在'))
}
}).catch(() => {
callback(new Error('校验标签名称失败'))
})
}
return {
//
groupList: [],
groupSearchInput: '',
activeGroupId: '',
activeGroup: null,
groupDialogVisible: false,
groupDialogType: 'add', // addedit
groupForm: {
id: '',
groupName: '',
tagNames: [],
originalName: '' //
},
groupRules: {
groupName: [
{ required: true, message: '请输入分组名称', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' },
{ validator: validateGroupName, trigger: 'blur' }
],
tagNames: [
{ required: true, message: '请输入至少一个标签名称', trigger: 'change' }
]
},
//
tagList: [],
tableLoading: false,
total: 0,
tagQueryParams: {
pageNum: 1,
pageSize: 10,
tagName: undefined,
groupId: null
},
tagDialogVisible: false,
tagDialogType: 'add', // addedit
tagForm: {
id: '',
groupId: '',
tagName: '',
names: [],
originalName: '' //
},
tagRules: {
groupId: [
{ required: true, message: '请选择分组', trigger: 'change' }
],
names: [
{ required: true, message: '请输入至少一个标签名称', trigger: 'change' },
{ validator: validateTagName, trigger: 'blur' }
]
}
}
},
computed: {
//
filteredGroupList() {
if (!this.groupSearchInput) {
return this.groupList
}
return this.groupList.filter(group =>
group.groupName.toLowerCase().includes(this.groupSearchInput.toLowerCase())
)
},
//
paginationTagList() {
let list = this.tagList
if (this.tagQueryParams.tagName) {
list = list.filter(tag =>
tag.tagName.toLowerCase().includes(this.tagQueryParams.tagName.toLowerCase())
)
}
this.total = list.length
const start = (this.tagQueryParams.pageNum - 1) * this.tagQueryParams.pageSize
const end = start + this.tagQueryParams.pageSize
return list.slice(start, end)
}
},
created() {
this.fetchGroupList()
},
methods: {
//
async fetchGroupList() {
try {
const res = await getAllTagGroups()
if (res.success) {
this.groupList = res.data || []
if (this.groupList.length > 0 && !this.activeGroupId) {
this.activeGroupId = this.groupList[0].id.toString()
this.activeGroup = this.groupList[0]
this.tagQueryParams.groupId = parseInt(this.activeGroupId)
this.fetchTagList(this.activeGroupId)
}
} else {
this.$message.error(res.message || '获取分组列表失败')
}
} catch (error) {
console.error('获取分组列表失败', error)
this.$message.error('获取分组列表失败')
}
},
//
handleGroupSearch() {
// filteredGroupList
},
handleGroupSelect(index) {
this.activeGroupId = index
this.activeGroup = this.groupList.find(group => group.id.toString() === index)
this.tagQueryParams.groupId = parseInt(index)
this.tagQueryParams.pageNum = 1
this.fetchTagList(index)
},
handleAddGroup() {
this.groupDialogType = 'add'
this.groupForm = {
id: '',
groupName: '',
tagNames: [],
originalName: ''
}
this.groupDialogVisible = true
},
handleEditGroup(group) {
this.groupDialogType = 'edit'
this.groupForm = {
id: group.id,
groupName: group.groupName,
tagNames: [],
originalName: group.groupName
}
this.groupDialogVisible = true
},
handleDeleteGroup(group) {
this.$confirm('此操作将永久删除该分组, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//
if (group.tagCount > 0) {
return this.$message.warning('该分组下存在标签,不能删除')
}
deleteTagGroup(group.id).then(res => {
if (res.success) {
this.$message.success('删除成功')
this.fetchGroupList()
if (this.activeGroupId === group.id.toString()) {
this.tagList = []
this.total = 0
}
} else {
this.$message.error(res.message || '删除失败')
}
}).catch(() => {
this.$message.error('删除失败')
})
}).catch(() => {
this.$message.info('已取消删除')
})
},
submitGroupForm() {
this.$refs.groupForm.validate(async (valid) => {
if (valid) {
try {
const data = {
groupName: this.groupForm.groupName
}
if (this.groupDialogType === 'add') {
const res = await addTagGroup(data)
if (res.success) {
this.$message.success('新增成功')
//
if (this.groupForm.tagNames && this.groupForm.tagNames.length > 0) {
//
await this.fetchGroupList()
const newGroup = this.groupList.find(g => g.groupName === this.groupForm.groupName)
if (newGroup) {
//
for (const tagName of this.groupForm.tagNames) {
await addTag({
groupId: newGroup.id,
tagName: tagName
})
}
}
}
} else {
this.$message.error(res.message || '新增分组失败')
}
} else {
data.id = this.groupForm.id
const res = await updateTagGroup(data)
if (res.success) {
this.$message.success('更新成功')
} else {
this.$message.error(res.message || '更新分组失败')
}
}
this.groupDialogVisible = false
await this.fetchGroupList()
} catch (error) {
console.error(this.groupDialogType === 'add' ? '新增分组失败' : '更新分组失败', error)
this.$message.error(this.groupDialogType === 'add' ? '新增分组失败' : '更新分组失败')
}
}
})
},
//
async fetchTagList(groupId) {
if (!groupId) return
this.tableLoading = true
try {
const res = await getTagListByGroupId(groupId)
if (res.success) {
this.tagList = res.data || []
this.total = this.tagList.length
this.tagQueryParams.pageNum = 1
} else {
this.$message.error(res.message || '获取标签列表失败')
this.tagList = []
this.total = 0
}
} catch (error) {
console.error('获取标签列表失败', error)
this.$message.error('获取标签列表失败')
this.tagList = []
this.total = 0
} finally {
this.tableLoading = false
}
},
//
handleTagQuery() {
//
if (this.tagQueryParams.tagName) {
this.fetchTagsByParams()
} else {
this.fetchTagList(this.activeGroupId)
}
},
//
async fetchTagsByParams() {
this.tableLoading = true
try {
const params = {
...this.tagQueryParams,
pageNum: 1,
pageSize: 1000 //
}
const res = await getTagList(params)
if (res.success) {
this.tagList = res.data.list || []
this.total = res.data.total || 0
} else {
this.$message.error(res.message || '查询标签失败')
this.tagList = []
this.total = 0
}
} catch (error) {
console.error('查询标签失败', error)
this.$message.error('查询标签失败')
this.tagList = []
this.total = 0
} finally {
this.tableLoading = false
}
},
//
resetTagQuery() {
this.tagQueryParams.tagName = undefined
this.tagQueryParams.pageNum = 1
this.fetchTagList(this.activeGroupId)
},
handleAddTag() {
this.tagDialogType = 'add'
this.tagForm = {
id: '',
groupId: this.activeGroupId,
tagName: '',
names: [],
originalName: ''
}
this.tagDialogVisible = true
},
handleEditTag(tag) {
this.tagDialogType = 'edit'
this.tagForm = {
id: tag.id,
groupId: tag.groupId,
tagName: tag.tagName,
names: [tag.tagName],
originalName: tag.tagName
}
this.tagDialogVisible = true
},
handleDeleteTag(tag) {
//
if (tag.intentionCustomerCount > 0) {
return this.$message.warning('该标签已绑定意向客户,不能删除')
}
this.$confirm('此操作将永久删除该标签, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteTag(tag.id).then(res => {
if (res.success) {
this.$message.success('删除成功')
this.fetchTagList(this.activeGroupId)
} else {
this.$message.error(res.message || '删除失败')
}
}).catch(() => {
this.$message.error('删除失败')
})
}).catch(() => {
this.$message.info('已取消删除')
})
},
submitTagForm() {
this.$refs.tagForm.validate(async (valid) => {
if (valid) {
try {
if (this.tagDialogType === 'add') {
//
if (this.tagForm.names.length > 1) {
let success = 0
let fail = 0
for (const name of this.tagForm.names) {
try {
const res = await addTag({
groupId: this.tagForm.groupId,
tagName: name
})
if (res.success) {
success++
} else {
fail++
}
} catch (error) {
fail++
}
}
if (success > 0) {
this.$message.success(`成功添加${success}个标签`)
}
if (fail > 0) {
this.$message.warning(`${fail}个标签添加失败`)
}
} else {
//
const res = await addTag({
groupId: this.tagForm.groupId,
tagName: this.tagForm.names[0]
})
if (res.success) {
this.$message.success('新增成功')
} else {
this.$message.error(res.message || '新增标签失败')
}
}
} else {
//
const res = await updateTag({
id: this.tagForm.id,
groupId: this.tagForm.groupId,
tagName: this.tagForm.names[0]
})
if (res.success) {
this.$message.success('更新成功')
} else {
this.$message.error(res.message || '更新标签失败')
}
}
this.tagDialogVisible = false
await this.fetchTagList(this.tagForm.groupId)
//
if (this.activeGroupId !== this.tagForm.groupId) {
this.activeGroupId = this.tagForm.groupId
this.activeGroup = this.groupList.find(group => group.id.toString() === this.tagForm.groupId)
}
} catch (error) {
console.error(this.tagDialogType === 'add' ? '新增标签失败' : '更新标签失败', error)
this.$message.error(this.tagDialogType === 'add' ? '新增标签失败' : '更新标签失败')
}
}
})
},
//
handleSizeChange(val) {
this.tagQueryParams.pageSize = val
},
handleCurrentChange(val) {
this.tagQueryParams.pageNum = val
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
.group-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
height: calc(100vh - 140px);
display: flex;
flex-direction: column;
}
.group-header {
padding: 15px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.group-header h3 {
margin: 0;
font-size: 16px;
}
.group-search {
padding: 10px 15px;
border-bottom: 1px solid #eee;
}
.group-menu {
flex: 1;
overflow-y: auto;
}
.group-item {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.group-actions {
display: none;
position: absolute;
right: 10px;
}
.group-item:hover .group-actions {
display: block;
}
.group-actions i {
margin-left: 8px;
font-size: 16px;
cursor: pointer;
color: #606266;
}
.group-actions i:hover {
color: #409EFF;
}
.group-actions .el-icon-delete:hover {
color: #F56C6C;
}
.tag-container {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
padding: 15px;
min-height: calc(100vh - 140px);
}
.tag-header {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
}
.header-title h3 {
margin: 0;
font-size: 16px;
margin-right: 10px;
}
.tag-search {
margin-bottom: 15px;
}
.search-form {
display: flex;
align-items: center;
}
.el-table {
margin-bottom: 15px;
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
.delete-btn {
color: #F56C6C;
}
.el-form-item-tip {
font-size: 12px;
color: #909399;
line-height: 1;
padding-top: 4px;
}
/* 确保弹窗样式一致 */
.el-dialog {
border-radius: 4px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -10,14 +10,14 @@ module.exports = {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
// proxy: {
// '/': {
// target: 'http://192.168.137.3:8080/api',
// changeOrigin: true,
// pathRewrite: {
// '^/api': '/api'
// }
// }
// }
}
}

Binary file not shown.

Binary file not shown.