功能说明
-
项目名称检索:
-
在输入框中输入内容时,会实时过滤项目列表
-
点击搜索图标也会触发过滤并展开下拉列表
-
-
下拉列表:
-
显示匹配的项目名称和分类
-
匹配文本高亮显示
-
带图标的项目展示
-
-
底部固定选项:
-
"自行选择商品编码"固定在底部
-
点击后打开商品编码选择对话框
-
-
商品编码选择:
-
以网格形式展示商品编码
-
选择后自动填充到项目名称字段
-
-
表格功能:
-
使用vxe-table实现高性能表格
-
金额自动计算
-
税率下拉选择器
-
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发票项目管理系统 - Element UI + vxe-table</title>
<!-- 引入Element UI样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入vxe-table样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vxe-table@3.9.1/lib/style.css">
<!-- 引入字体图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!-- 修复VXETable未定义的问题 -->
<script src="https://cdn.jsdelivr.net/npm/xe-utils"></script>
<script src="https://cdn.jsdelivr.net/npm/vxe-pc-ui@3.1.27"></script>
<script src="https://cdn.jsdelivr.net/npm/vxe-table@3.9.1"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f0f5ff 0%, #e6f7ff 100%);
min-height: 100vh;
padding: 20px;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.header {
background: linear-gradient(to right, #1a6dcc, #0d4a9e);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.header-actions {
display: flex;
gap: 15px;
}
.header-actions .el-button {
background: rgba(255, 255, 255, 0.15);
border: none;
color: white;
transition: all 0.3s;
}
.header-actions .el-button:hover {
background: rgba(255, 255, 255, 0.25);
}
.toolbar {
padding: 15px 20px;
background: #f8fafd;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.toolbar-actions {
display: flex;
gap: 10px;
}
.vxe-table-container {
padding: 20px;
}
.vxe-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.vxe-table .vxe-header--column {
background-color: #f5f9ff;
color: #1a6dcc;
font-weight: 600;
}
.vxe-table .vxe-body--column {
padding: 12px 10px;
}
.footer {
padding: 20px;
background: #f8fafd;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.total-section {
display: flex;
align-items: center;
gap: 20px;
}
.total-label {
font-weight: 600;
color: #666;
}
.total-amount {
font-size: 22px;
font-weight: 700;
color: #e74c3c;
}
.tax-section {
color: #666;
font-size: 14px;
line-height: 1.6;
}
.add-item-btn {
background: linear-gradient(to right, #2ecc71, #27ae60);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
}
.add-item-btn:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.custom-autocomplete {
width: 100%;
}
.suggestion-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
transition: all 0.2s;
}
.suggestion-item:hover {
background-color: #f0f7ff;
}
.suggestion-icon {
margin-right: 10px;
color: #1a6dcc;
min-width: 20px;
text-align: center;
}
.suggestion-content {
flex: 1;
overflow: hidden;
}
.suggestion-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.suggestion-category {
font-size: 12px;
color: #888;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fixed-option {
background: #f8fafd;
border-top: 1px solid #eee;
font-weight: 600;
color: #1a6dcc;
padding: 10px 12px;
display: flex;
align-items: center;
cursor: pointer;
}
.fixed-option:hover {
background: #e6f0ff;
}
.code-modal {
border-radius: 12px;
overflow: hidden;
}
.code-modal-header {
background: linear-gradient(to right, #1a6dcc, #0d4a9e);
color: white;
padding: 15px 20px;
font-size: 18px;
font-weight: 600;
}
.code-grid {
padding: 20px;
max-height: 400px;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.code-item {
border: 1px solid #e1e4e8;
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.2s;
background: white;
}
.code-item:hover {
border-color: #1a6dcc;
box-shadow: 0 4px 10px rgba(26, 109, 204, 0.15);
transform: translateY(-3px);
}
.code-name {
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.code-id {
font-size: 13px;
color: #666;
}
.code-category {
font-size: 12px;
color: #888;
margin-top: 5px;
}
.highlight {
color: #e74c3c;
font-weight: 600;
}
.el-popper.custom-popper {
max-height: 300px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: none;
border-radius: 8px;
padding: 0;
}
.vxe-table .vxe-body--row:hover {
background-color: #f8fbff;
}
.amount-cell {
font-weight: 500;
color: #1a6dcc;
}
.el-input__inner {
border-radius: 6px;
}
.el-select {
width: 100%;
}
</style>
</head>
<body>
<div id="app">
<div class="app-container">
<div class="header">
<h1>
<i class="fas fa-file-invoice"></i>
发票项目管理系统
</h1>
<div class="header-actions">
<el-button icon="el-icon-time">开通时间</el-button>
<el-button icon="el-icon-document">待发单</el-button>
<el-button icon="el-icon-tickets">待发单</el-button>
</div>
</div>
<div class="toolbar">
<div class="toolbar-title">发票项目明细</div>
<div class="toolbar-actions">
<el-button type="primary" icon="el-icon-plus" @click="addNewItem">新增行</el-button>
<el-button type="danger" icon="el-icon-delete" @click="deleteItem">删除</el-button>
<el-button icon="el-icon-upload2">明细导入</el-button>
<el-button icon="el-icon-discount">对应折扣</el-button>
</div>
</div>
<div class="vxe-table-container">
<vxe-table
:data="tableData"
border="inner"
show-overflow
auto-resize
highlight-hover-row
ref="invoiceTable"
>
<vxe-column type="seq" width="60" title="序号"></vxe-column>
<vxe-column field="projectName" title="项目名称" min-width="220">
<template v-slot="{ row }">
<el-autocomplete
class="custom-autocomplete"
v-model="row.projectName"
:fetch-suggestions="querySearch"
placeholder="请输入项目名称"
@focus="handleFocus(row)"
@select="handleSelect"
value-key="name"
popper-class="custom-popper"
:highlight-first-item="true"
:trigger-on-focus="true"
>
<template v-slot:default="{ item }">
<div class="suggestion-item">
<div class="suggestion-content">
<div class="suggestion-name">{{ item.name}}</div>
</div>
</div>
</template>
<template v-slot:footer>
<div class="fixed-option" @click="showCodeDialog(row)">
<i class="fas fa-barcode"></i>
<span style="margin-left: 8px;">自行选择商品编码</span>
</div>
</template>
</el-autocomplete>
</template>
</vxe-column>
<vxe-column field="specification" title="规格型号" width="150">
<template v-slot="{ row }">
<el-input v-model="row.specification" placeholder="规格型号" size="small"></el-input>
</template>
</vxe-column>
<vxe-column field="unit" title="单位" width="100">
<template v-slot="{ row }">
<el-input v-model="row.unit" placeholder="单位" size="small"></el-input>
</template>
</vxe-column>
<vxe-column field="quantity" title="数量" width="100">
<template v-slot="{ row }">
<el-input-number
v-model="row.quantity"
:min="0"
:precision="2"
controls-position="right"
size="small"
@change="calculateRowAmount(row)"
></el-input-number>
</template>
</vxe-column>
<vxe-column field="price" title="单价(含税)" width="150">
<template v-slot="{ row }">
<el-input
v-model="row.price"
placeholder="0.00"
size="small"
@input="calculateRowAmount(row)"
>
<template slot="prepend">¥</template>
</el-input>
</template>
</vxe-column>
<vxe-column field="amount" title="金额(含税)" width="150">
<template v-slot="{ row }">
<div class="amount-cell">¥ {{ row.amount }}</div>
</template>
</vxe-column>
<vxe-column field="taxRate" title="税率/征收率" width="150">
<template v-slot="{ row }">
<el-select v-model="row.taxRate" placeholder="选择税率" size="small">
<el-option label="13%" value="13%"></el-option>
<el-option label="9%" value="9%"></el-option>
<el-option label="6%" value="6%"></el-option>
<el-option label="3%" value="3%"></el-option>
<el-option label="0%" value="0%"></el-option>
</el-select>
</template>
</vxe-column>
</vxe-table>
</div>
<div class="footer">
<div class="total-section">
<div class="total-label">合计:</div>
<div class="total-amount">¥ {{ totalAmount }}</div>
</div>
<div class="tax-section">
<div>*现代服务*现代服务费</div>
<div>*交通运输设备*等自行车及</div>
<div>*交通运输设备*普通通行车</div>
</div>
<button class="add-item-btn" @click="addNewItem">
<i class="fas fa-plus-circle"></i> 添加新项目
</button>
</div>
</div>
<!-- 商品编码选择对话框 -->
<el-dialog
title="选择商品编码"
:visible.sync="codeDialogVisible"
width="800px"
custom-class="code-modal"
:close-on-click-modal="false"
>
<div class="code-modal-header">请选择商品编码</div>
<div class="code-grid">
<div
v-for="(code, index) in commodityCodes"
:key="index"
class="code-item"
@click="selectCode(code)"
>
<div class="code-name">{{ code.name }}</div>
<div class="code-id">编码: {{ code.id }}</div>
<div class="code-category">{{ code.category }}</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="codeDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="codeDialogVisible = false">确 定</el-button>
</span>
</el-dialog>
</div>
<script>
// 修复VXETable未定义的问题
Vue.use(VXETable);
new Vue({
el: '#app',
data() {
return {
tableData: [
{
id: 1,
projectName: '',
specification: '',
unit: '',
quantity: 1,
price: '',
amount: '0.00',
taxRate: '13%'
}
],
projectSuggestions: [
{ name: "办公用品", category: "办公设备", icon: "fas fa-cube" },
{ name: "技术服务费", category: "现代服务", icon: "fas fa-laptop-code" },
{ name: "咨询服务", category: "现代服务", icon: "fas fa-headset" },
{ name: "软件开发", category: "信息技术", icon: "fas fa-code" },
{ name: "电脑设备", category: "电子产品", icon: "fas fa-desktop" },
{ name: "网络服务", category: "信息技术", icon: "fas fa-network-wired" },
{ name: "广告服务", category: "文化传媒", icon: "fas fa-ad" },
{ name: "维修服务", category: "技术服务", icon: "fas fa-tools" },
{ name: "培训服务", category: "教育服务", icon: "fas fa-chalkboard-teacher" },
{ name: "物流服务", category: "运输服务", icon: "fas fa-truck" },
{ name: "会议服务", category: "商务服务", icon: "fas fa-conference" },
{ name: "租赁服务", category: "商业服务", icon: "fas fa-key" }
],
commodityCodes: [
{ id: "109010101", name: "办公用品", category: "办公设备" },
{ id: "304020202", name: "技术服务", category: "现代服务" },
{ id: "304030303", name: "咨询服务", category: "现代服务" },
{ id: "108050505", name: "软件开发", category: "信息技术" },
{ id: "109030303", name: "电脑设备", category: "电子产品" },
{ id: "108040404", name: "网络服务", category: "信息技术" },
{ id: "306010101", name: "广告服务", category: "文化传媒" },
{ id: "304040404", name: "维修服务", category: "技术服务" },
{ id: "307020202", name: "培训服务", category: "教育服务" },
{ id: "302010101", name: "物流服务", category: "运输服务" },
{ id: "304050505", name: "会议服务", category: "商务服务" },
{ id: "304060606", name: "租赁服务", category: "商业服务" }
],
codeDialogVisible: false,
activeRow: null
};
},
computed: {
totalAmount() {
return this.tableData.reduce((total, row) => {
return total + parseFloat(row.amount || 0);
}, 0).toFixed(2);
}
},
methods: {
calculateRowAmount(row) {
const quantity = parseFloat(row.quantity) || 0;
const price = parseFloat(row.price) || 0;
row.amount = (quantity * price).toFixed(2);
},
querySearch(queryString, cb) {
const results = queryString
? this.projectSuggestions.filter(item =>
item.name.toLowerCase().includes(queryString.toLowerCase()) ||
item.category.toLowerCase().includes(queryString.toLowerCase())
)
: this.projectSuggestions;
// 调用 callback 返回建议列表的数据
cb(results);
},
handleFocus(row) {
this.activeRow = row;
},
handleSelect(item) {
if (this.activeRow) {
this.activeRow.xmmc = item.nsrmc;
}
},
highlightMatch(text, query) {
if (!query || !text) return text;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const startIndex = lowerText.indexOf(lowerQuery);
if (startIndex === -1) return text;
const endIndex = startIndex + query.length;
const before = text.substring(0, startIndex);
const match = text.substring(startIndex, endIndex);
const after = text.substring(endIndex);
return `${before}<span class="highlight">${match}</span>${after}`;
},
showCodeDialog(row) {
this.activeRow = row;
this.codeDialogVisible = true;
},
selectCode(code) {
if (this.activeRow) {
this.activeRow.projectName = code.name;
}
this.codeDialogVisible = false;
},
addNewItem() {
const newId = this.tableData.length > 0
? Math.max(...this.tableData.map(item => item.id)) + 1
: 1;
this.tableData.push({
id: newId,
projectName: '',
specification: '',
unit: '',
quantity: 1,
price: '',
amount: '0.00',
taxRate: '13%'
});
},
deleteItem() {
if (this.tableData.length > 1) {
this.tableData.pop();
} else {
this.$message.warning('至少保留一行数据');
}
}
}
});
</script>
</body>
</html>