Commit b8a578c8 by suyuchen

chore(docs): 移除停机问题处理和VIP客户服务相关文件

- 删除停机问题处理流程制定相关的文档和页面文件 - 移除VIP客户服务机制建立的HTML页面文件 - 清理相关的统计
parent 7c1743b7
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP客户管理 - VIP客户服务机制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-bar {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 150px;
}
.filter-item label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
.filter-item select,
.filter-item input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-warning {
background: #f39c12;
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
.btn-sm {
padding: 6px 12px;
font-size: 14px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.data-table th {
background: #f8f9fa;
font-weight: bold;
color: #333;
}
.data-table tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.badge-vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
}
.badge-svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
}
.badge-normal {
background: #95a5a6;
color: white;
}
.badge-success {
background: #27ae60;
color: white;
}
.badge-danger {
background: #e74c3c;
color: white;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 15px;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card.vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
}
.stat-card.svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>⭐ VIP客户管理</h1>
<div class="nav-menu">
<a href="1-VIP客户管理列表页.html" class="nav-item active">VIP客户管理</a>
<a href="3-VIP专属负责人配置页.html" class="nav-item">负责人配置</a>
<a href="4-VIP服务工单列表页.html" class="nav-item">VIP工单</a>
<a href="5-VIP SLA规则配置页.html" class="nav-item">SLA配置</a>
<a href="6-VIP客户服务记录页.html" class="nav-item">服务记录</a>
<a href="7-VIP客户服务看板.html" class="nav-item">服务看板</a>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-bar">
<div class="stat-card">
<div class="stat-label">总客户数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
<div class="stat-card vip">
<div class="stat-label">VIP客户</div>
<div class="stat-value">{{ stats.vip }}</div>
</div>
<div class="stat-card svip">
<div class="stat-label">SVIP客户</div>
<div class="stat-value">{{ stats.svip }}</div>
</div>
<div class="stat-card">
<div class="stat-label">已启用VIP</div>
<div class="stat-value">{{ stats.enabled }}</div>
</div>
</div>
<div class="card">
<div class="card-title">
<span>VIP客户列表 (共 {{ filteredCustomers.length }} 条)</span>
<button @click="openAddDialog" class="btn btn-primary">
➕ 新增VIP客户
</button>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item">
<label>客户级别</label>
<select v-model="filters.level" @change="applyFilters">
<option value="">全部级别</option>
<option value="normal">普通</option>
<option value="vip">VIP</option>
<option value="svip">SVIP</option>
</select>
</div>
<div class="filter-item">
<label>状态筛选</label>
<select v-model="filters.status" @change="applyFilters">
<option value="">全部状态</option>
<option value="enabled">已启用</option>
<option value="disabled">已停用</option>
</select>
</div>
<div class="filter-item">
<label>客户名称</label>
<input type="text" v-model="filters.name" @input="applyFilters" placeholder="输入客户名称">
</div>
<div class="filter-item">
<label>专属负责人</label>
<input type="text" v-model="filters.manager" @input="applyFilters" placeholder="输入负责人">
</div>
</div>
<!-- 数据表格 -->
<table class="data-table" v-if="filteredCustomers.length > 0">
<thead>
<tr>
<th>客户名称</th>
<th>客户级别</th>
<th>VIP等级</th>
<th>专属负责人</th>
<th>VIP生效时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="customer in filteredCustomers" :key="customer.id">
<td>{{ customer.name }}</td>
<td>
<span class="badge" :class="customer.level === 'normal' ? 'badge-normal' : (customer.level === 'vip' ? 'badge-vip' : 'badge-svip')">
{{ customer.level === 'normal' ? '普通' : (customer.level === 'vip' ? 'VIP' : 'SVIP') }}
</span>
</td>
<td>
<span class="badge" :class="customer.vipLevel === 'vip' ? 'badge-vip' : 'badge-svip'" v-if="customer.level !== 'normal'">
{{ customer.vipLevel === 'vip' ? 'VIP' : 'SVIP' }}
</span>
<span v-else>-</span>
</td>
<td>{{ customer.manager || '-' }}</td>
<td>{{ customer.effectiveTime ? formatTime(customer.effectiveTime) : '-' }}</td>
<td>
<span class="badge" :class="customer.status === 'enabled' ? 'badge-success' : 'badge-danger'">
{{ customer.status === 'enabled' ? '已启用' : '已停用' }}
</span>
</td>
<td>
<button @click="editCustomer(customer)" class="btn btn-primary btn-sm">编辑</button>
<button
@click="toggleStatus(customer)"
class="btn btn-sm"
:class="customer.status === 'enabled' ? 'btn-warning' : 'btn-success'"
>
{{ customer.status === 'enabled' ? '停用' : '启用' }}
</button>
</td>
</tr>
</tbody>
</table>
<div class="empty-state" v-else>
<div class="empty-state-icon">👥</div>
<div>暂无符合条件的客户</div>
</div>
</div>
</div>
<!-- 新增/编辑对话框 -->
<div v-if="showDialog" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
<div class="card" style="width: 600px; max-height: 90vh; overflow-y: auto;">
<div class="card-title">
<span>{{ editingCustomer.id ? '编辑' : '新增' }}VIP客户</span>
<button @click="closeDialog" style="background: #e74c3c; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;"></button>
</div>
<form @submit.prevent="saveCustomer">
<div class="filter-item" style="margin-bottom: 15px;">
<label>客户名称 *</label>
<select v-model="editingCustomer.name" required style="width: 100%; padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px;">
<option value="">请选择客户</option>
<option v-for="name in availableCustomers" :key="name" :value="name">{{ name }}</option>
</select>
</div>
<div class="filter-item" style="margin-bottom: 15px;">
<label>客户级别 *</label>
<select v-model="editingCustomer.level" required style="width: 100%; padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px;">
<option value="normal">普通</option>
<option value="vip">VIP</option>
<option value="svip">SVIP</option>
</select>
</div>
<div class="filter-item" style="margin-bottom: 15px;" v-if="editingCustomer.level !== 'normal'">
<label>VIP等级 *</label>
<select v-model="editingCustomer.vipLevel" required style="width: 100%; padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px;">
<option value="vip">VIP</option>
<option value="svip">SVIP</option>
</select>
</div>
<div class="filter-item" style="margin-bottom: 15px;">
<label>VIP生效时间</label>
<input type="datetime-local" v-model="editingCustomer.effectiveTimeStr" style="width: 100%; padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px;">
</div>
<div class="filter-item" style="margin-bottom: 15px;">
<label>VIP失效时间</label>
<input type="datetime-local" v-model="editingCustomer.expireTimeStr" style="width: 100%; padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px;">
</div>
<div class="filter-item" style="margin-bottom: 15px;">
<label>备注说明</label>
<textarea v-model="editingCustomer.remark" rows="4" style="width: 100%; padding: 8px 12px; border: 2px solid #e0e0e0; border-radius: 6px; resize: vertical;"></textarea>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
<button type="button" @click="closeDialog" class="btn" style="background: #95a5a6; color: white;">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
customers: [],
filters: {
level: '',
status: '',
name: '',
manager: ''
},
showDialog: false,
editingCustomer: {
id: null,
name: '',
level: 'normal',
vipLevel: 'vip',
effectiveTime: null,
effectiveTimeStr: '',
expireTime: null,
expireTimeStr: '',
remark: '',
status: 'enabled'
},
availableCustomers: ['客户A', '客户B', '客户C', '客户D', '客户E', '客户F', '客户G', '客户H']
}
},
computed: {
stats() {
return {
total: this.customers.length,
vip: this.customers.filter(c => c.level === 'vip').length,
svip: this.customers.filter(c => c.level === 'svip').length,
enabled: this.customers.filter(c => c.status === 'enabled').length
};
},
filteredCustomers() {
let result = [...this.customers];
if (this.filters.level) {
result = result.filter(c => c.level === this.filters.level);
}
if (this.filters.status) {
result = result.filter(c => c.status === this.filters.status);
}
if (this.filters.name) {
result = result.filter(c =>
c.name.toLowerCase().includes(this.filters.name.toLowerCase())
);
}
if (this.filters.manager) {
result = result.filter(c =>
c.manager && c.manager.toLowerCase().includes(this.filters.manager.toLowerCase())
);
}
return result;
}
},
mounted() {
this.loadCustomers();
},
methods: {
loadCustomers() {
const saved = localStorage.getItem('vipCustomers');
if (saved) {
this.customers = JSON.parse(saved);
} else {
// 初始化示例数据
this.customers = [
{
id: 1,
name: '客户A',
level: 'vip',
vipLevel: 'vip',
manager: '张三',
effectiveTime: Date.now() - 30 * 24 * 60 * 60 * 1000,
expireTime: null,
remark: '重要VIP客户',
status: 'enabled'
},
{
id: 2,
name: '客户B',
level: 'svip',
vipLevel: 'svip',
manager: '李四',
effectiveTime: Date.now() - 60 * 24 * 60 * 60 * 1000,
expireTime: null,
remark: '超级VIP客户',
status: 'enabled'
}
];
this.saveCustomers();
}
},
saveCustomers() {
localStorage.setItem('vipCustomers', JSON.stringify(this.customers));
},
applyFilters() {
// 筛选逻辑在computed中处理
},
openAddDialog() {
this.editingCustomer = {
id: null,
name: '',
level: 'normal',
vipLevel: 'vip',
effectiveTime: null,
effectiveTimeStr: '',
expireTime: null,
expireTimeStr: '',
remark: '',
status: 'enabled'
};
this.showDialog = true;
},
editCustomer(customer) {
this.editingCustomer = {
...customer,
effectiveTimeStr: customer.effectiveTime ? new Date(customer.effectiveTime).toISOString().slice(0, 16) : '',
expireTimeStr: customer.expireTime ? new Date(customer.expireTime).toISOString().slice(0, 16) : ''
};
this.showDialog = true;
},
closeDialog() {
this.showDialog = false;
},
saveCustomer() {
const customer = {
...this.editingCustomer,
effectiveTime: this.editingCustomer.effectiveTimeStr ? new Date(this.editingCustomer.effectiveTimeStr).getTime() : null,
expireTime: this.editingCustomer.expireTimeStr ? new Date(this.editingCustomer.expireTimeStr).getTime() : null
};
if (customer.id) {
const index = this.customers.findIndex(c => c.id === customer.id);
if (index !== -1) {
this.customers[index] = customer;
}
} else {
customer.id = Date.now();
this.customers.push(customer);
}
this.saveCustomers();
this.closeDialog();
alert('保存成功');
},
toggleStatus(customer) {
customer.status = customer.status === 'enabled' ? 'disabled' : 'enabled';
this.saveCustomers();
alert(customer.status === 'enabled' ? '已启用' : '已停用');
},
formatTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN');
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP客户新增/编辑 - VIP客户服务机制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 30px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
font-weight: 500;
}
.form-group label.required::after {
content: ' *';
color: #e74c3c;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: flex-end;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.help-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.vip-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-left: 10px;
}
.badge-vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
}
.badge-svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>⭐ {{ isEdit ? '编辑' : '新增' }}VIP客户</h1>
<div class="nav-menu">
<a href="1-VIP客户管理列表页.html" class="nav-item">VIP客户管理</a>
<a href="3-VIP专属负责人配置页.html" class="nav-item">负责人配置</a>
<a href="4-VIP服务工单列表页.html" class="nav-item">VIP工单</a>
<a href="5-VIP SLA规则配置页.html" class="nav-item">SLA配置</a>
<a href="6-VIP客户服务记录页.html" class="nav-item">服务记录</a>
<a href="7-VIP客户服务看板.html" class="nav-item">服务看板</a>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
{{ isEdit ? '编辑VIP客户信息' : '新增VIP客户信息' }}
</div>
<form @submit.prevent="saveCustomer">
<div class="form-group">
<label class="required">客户名称</label>
<select v-model="formData.name" required>
<option value="">请选择客户</option>
<option v-for="name in availableCustomers" :key="name" :value="name">{{ name }}</option>
</select>
<div class="help-text">从现有客户列表中选择</div>
</div>
<div class="form-group">
<label class="required">客户级别</label>
<select v-model="formData.level" required @change="onLevelChange">
<option value="normal">普通</option>
<option value="vip">VIP</option>
<option value="svip">SVIP</option>
</select>
<div class="help-text">选择客户的级别类型</div>
</div>
<div class="form-group" v-if="formData.level !== 'normal'">
<label class="required">VIP等级</label>
<select v-model="formData.vipLevel" required>
<option value="vip">VIP</option>
<option value="svip">SVIP</option>
</select>
<div class="help-text">
VIP:标准VIP客户
<span class="vip-badge badge-vip">VIP</span>
<br>
SVIP:超级VIP客户,享受最高优先级服务
<span class="vip-badge badge-svip">SVIP</span>
</div>
</div>
<div class="form-group">
<label>VIP生效时间</label>
<input type="datetime-local" v-model="formData.effectiveTimeStr">
<div class="help-text">VIP状态开始生效的时间</div>
</div>
<div class="form-group">
<label>VIP失效时间</label>
<input type="datetime-local" v-model="formData.expireTimeStr">
<div class="help-text">VIP状态失效的时间(留空表示永久有效)</div>
</div>
<div class="form-group">
<label>备注说明</label>
<textarea v-model="formData.remark" placeholder="请输入备注信息,如:客户重要程度、特殊要求等"></textarea>
<div class="help-text">可填写客户的特殊说明或要求</div>
</div>
<div class="form-actions">
<button type="button" @click="cancel" class="btn btn-secondary">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
isEdit: false,
customerId: null,
formData: {
name: '',
level: 'normal',
vipLevel: 'vip',
effectiveTime: null,
effectiveTimeStr: '',
expireTime: null,
expireTimeStr: '',
remark: '',
status: 'enabled'
},
availableCustomers: ['客户A', '客户B', '客户C', '客户D', '客户E', '客户F', '客户G', '客户H']
}
},
mounted() {
// 从URL参数获取编辑信息
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
if (id) {
this.isEdit = true;
this.customerId = parseInt(id);
this.loadCustomer();
}
},
methods: {
loadCustomer() {
const saved = localStorage.getItem('vipCustomers');
if (saved) {
const customers = JSON.parse(saved);
const customer = customers.find(c => c.id === this.customerId);
if (customer) {
this.formData = {
...customer,
effectiveTimeStr: customer.effectiveTime ? new Date(customer.effectiveTime).toISOString().slice(0, 16) : '',
expireTimeStr: customer.expireTime ? new Date(customer.expireTime).toISOString().slice(0, 16) : ''
};
}
}
},
onLevelChange() {
if (this.formData.level === 'normal') {
this.formData.vipLevel = 'vip';
}
},
saveCustomer() {
const customer = {
...this.formData,
id: this.isEdit ? this.customerId : Date.now(),
effectiveTime: this.formData.effectiveTimeStr ? new Date(this.formData.effectiveTimeStr).getTime() : null,
expireTime: this.formData.expireTimeStr ? new Date(this.formData.expireTimeStr).getTime() : null
};
const saved = localStorage.getItem('vipCustomers');
let customers = saved ? JSON.parse(saved) : [];
if (this.isEdit) {
const index = customers.findIndex(c => c.id === this.customerId);
if (index !== -1) {
customers[index] = customer;
}
} else {
customers.push(customer);
}
localStorage.setItem('vipCustomers', JSON.stringify(customers));
alert('保存成功');
window.location.href = '1-VIP客户管理列表页.html';
},
cancel() {
if (confirm('确定要取消吗?未保存的数据将丢失。')) {
window.location.href = '1-VIP客户管理列表页.html';
}
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP专属负责人配置 - VIP客户服务机制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.data-table th {
background: #f8f9fa;
font-weight: bold;
color: #333;
}
.data-table tr:hover {
background: #f8f9fa;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-sm {
padding: 6px 12px;
font-size: 14px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: flex-end;
margin-top: 20px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog-content {
background: white;
border-radius: 10px;
padding: 30px;
width: 600px;
max-height: 90vh;
overflow-y: auto;
}
.badge-vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.badge-svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>👤 VIP专属负责人配置</h1>
<div class="nav-menu">
<a href="1-VIP客户管理列表页.html" class="nav-item">VIP客户管理</a>
<a href="3-VIP专属负责人配置页.html" class="nav-item active">负责人配置</a>
<a href="4-VIP服务工单列表页.html" class="nav-item">VIP工单</a>
<a href="5-VIP SLA规则配置页.html" class="nav-item">SLA配置</a>
<a href="6-VIP客户服务记录页.html" class="nav-item">服务记录</a>
<a href="7-VIP客户服务看板.html" class="nav-item">服务看板</a>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
<span>VIP客户专属负责人配置 (共 {{ managers.length }} 条)</span>
<button @click="openAddDialog" class="btn btn-primary">
➕ 新增配置
</button>
</div>
<table class="data-table" v-if="managers.length > 0">
<thead>
<tr>
<th>客户名称</th>
<th>VIP等级</th>
<th>主负责人</th>
<th>备份负责人</th>
<th>负责人联系方式</th>
<th>负责人部门</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="manager in managers" :key="manager.id">
<td>{{ manager.customerName }}</td>
<td>
<span :class="manager.vipLevel === 'vip' ? 'badge-vip' : 'badge-svip'">
{{ manager.vipLevel === 'vip' ? 'VIP' : 'SVIP' }}
</span>
</td>
<td>{{ manager.mainManager }}</td>
<td>{{ manager.backupManager || '-' }}</td>
<td>{{ manager.contact }}</td>
<td>{{ manager.department }}</td>
<td>
<button @click="editManager(manager)" class="btn btn-primary btn-sm">编辑</button>
<button @click="deleteManager(manager.id)" class="btn btn-sm" style="background: #e74c3c; color: white;">删除</button>
</td>
</tr>
</tbody>
</table>
<div class="empty-state" v-else>
<div class="empty-state-icon">👥</div>
<div>暂无负责人配置</div>
</div>
</div>
</div>
<!-- 新增/编辑对话框 -->
<div v-if="showDialog" class="dialog-overlay" @click.self="closeDialog">
<div class="dialog-content">
<div class="card-title">
<span>{{ editingManager.id ? '编辑' : '新增' }}负责人配置</span>
<button @click="closeDialog" style="background: #e74c3c; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;"></button>
</div>
<form @submit.prevent="saveManager">
<div class="form-group">
<label>客户名称 *</label>
<select v-model="editingManager.customerName" required>
<option value="">请选择客户</option>
<option v-for="customer in vipCustomers" :key="customer.id" :value="customer.name">
{{ customer.name }}
</option>
</select>
</div>
<div class="form-group">
<label>VIP等级 *</label>
<select v-model="editingManager.vipLevel" required>
<option value="vip">VIP</option>
<option value="svip">SVIP</option>
</select>
</div>
<div class="form-group">
<label>主负责人 *</label>
<input type="text" v-model="editingManager.mainManager" required placeholder="请输入主负责人姓名">
</div>
<div class="form-group">
<label>备份负责人</label>
<input type="text" v-model="editingManager.backupManager" placeholder="请输入备份负责人姓名(可选)">
</div>
<div class="form-group">
<label>负责人联系方式 *</label>
<input type="text" v-model="editingManager.contact" required placeholder="请输入联系电话或邮箱">
</div>
<div class="form-group">
<label>负责人部门 *</label>
<select v-model="editingManager.department" required>
<option value="">请选择部门</option>
<option value="技术支持部">技术支持部</option>
<option value="客户服务部">客户服务部</option>
<option value="销售部">销售部</option>
<option value="产品部">产品部</option>
<option value="运营部">运营部</option>
</select>
</div>
<div class="form-actions">
<button type="button" @click="closeDialog" class="btn" style="background: #95a5a6; color: white;">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
managers: [],
showDialog: false,
editingManager: {
id: null,
customerName: '',
vipLevel: 'vip',
mainManager: '',
backupManager: '',
contact: '',
department: ''
},
vipCustomers: []
}
},
mounted() {
this.loadVipCustomers();
this.loadManagers();
},
methods: {
loadVipCustomers() {
const saved = localStorage.getItem('vipCustomers');
if (saved) {
this.vipCustomers = JSON.parse(saved).filter(c => c.level !== 'normal');
}
},
loadManagers() {
const saved = localStorage.getItem('vipManagers');
if (saved) {
this.managers = JSON.parse(saved);
} else {
// 初始化示例数据
this.managers = [
{
id: 1,
customerName: '客户A',
vipLevel: 'vip',
mainManager: '张三',
backupManager: '李四',
contact: '13800138000',
department: '技术支持部'
},
{
id: 2,
customerName: '客户B',
vipLevel: 'svip',
mainManager: '王五',
backupManager: '赵六',
contact: '13900139000',
department: '客户服务部'
}
];
this.saveManagers();
}
},
saveManagers() {
localStorage.setItem('vipManagers', JSON.stringify(this.managers));
},
openAddDialog() {
this.editingManager = {
id: null,
customerName: '',
vipLevel: 'vip',
mainManager: '',
backupManager: '',
contact: '',
department: ''
};
this.showDialog = true;
},
editManager(manager) {
this.editingManager = { ...manager };
this.showDialog = true;
},
closeDialog() {
this.showDialog = false;
},
saveManager() {
const manager = { ...this.editingManager };
if (manager.id) {
const index = this.managers.findIndex(m => m.id === manager.id);
if (index !== -1) {
this.managers[index] = manager;
}
} else {
manager.id = Date.now();
this.managers.push(manager);
}
this.saveManagers();
this.closeDialog();
alert('保存成功');
},
deleteManager(id) {
if (confirm('确定要删除这条配置吗?')) {
this.managers = this.managers.filter(m => m.id !== id);
this.saveManagers();
alert('删除成功');
}
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP服务工单列表 - VIP客户服务机制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.filter-bar {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 150px;
}
.filter-item label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
.filter-item select,
.filter-item input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-sm {
padding: 6px 12px;
font-size: 14px;
}
.ticket-list {
max-height: 700px;
overflow-y: auto;
}
.ticket-item {
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s;
background: #f9f9f9;
cursor: pointer;
position: relative;
}
.ticket-item.vip {
border-color: #f39c12;
background: linear-gradient(135deg, #fff9e6 0%, #fff5d6 100%);
}
.ticket-item.svip {
border-color: #e74c3c;
background: linear-gradient(135deg, #ffeaea 0%, #ffd6d6 100%);
}
.ticket-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.ticket-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.ticket-id {
font-weight: bold;
color: #667eea;
font-size: 16px;
}
.vip-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-left: 10px;
}
.vip-badge.vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
}
.vip-badge.svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
}
.ticket-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.status-pending {
background: #f39c12;
color: white;
}
.status-processing {
background: #3498db;
color: white;
}
.status-resolved {
background: #27ae60;
color: white;
}
.status-closed {
background: #95a5a6;
color: white;
}
.priority-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-left: 10px;
}
.priority-high {
background: #e74c3c;
color: white;
}
.priority-medium {
background: #f39c12;
color: white;
}
.priority-low {
background: #3498db;
color: white;
}
.ticket-info {
margin: 10px 0;
color: #666;
font-size: 14px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
}
.ticket-info strong {
color: #333;
}
.sla-timer {
display: inline-block;
padding: 6px 12px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
font-weight: bold;
color: #856404;
margin-top: 10px;
}
.sla-timer.warning {
background: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.sla-timer.timeout {
background: #721c24;
border-color: #dc3545;
color: white;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.ticket-actions {
margin-top: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 15px;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card.vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
}
.stat-card.svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>🎫 VIP服务工单列表</h1>
<div class="nav-menu">
<a href="1-VIP客户管理列表页.html" class="nav-item">VIP客户管理</a>
<a href="3-VIP专属负责人配置页.html" class="nav-item">负责人配置</a>
<a href="4-VIP服务工单列表页.html" class="nav-item active">VIP工单</a>
<a href="5-VIP SLA规则配置页.html" class="nav-item">SLA配置</a>
<a href="6-VIP客户服务记录页.html" class="nav-item">服务记录</a>
<a href="7-VIP客户服务看板.html" class="nav-item">服务看板</a>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-bar">
<div class="stat-card">
<div class="stat-label">总工单数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
<div class="stat-card vip">
<div class="stat-label">VIP工单</div>
<div class="stat-value">{{ stats.vip }}</div>
</div>
<div class="stat-card svip">
<div class="stat-label">SVIP工单</div>
<div class="stat-value">{{ stats.svip }}</div>
</div>
<div class="stat-card">
<div class="stat-label">待处理</div>
<div class="stat-value">{{ stats.pending }}</div>
</div>
</div>
<div class="card">
<div class="card-title">
VIP服务工单列表 (共 {{ filteredTickets.length }} 条)
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item">
<label>工单状态</label>
<select v-model="filters.status" @change="applyFilters">
<option value="">全部状态</option>
<option value="pending">待响应</option>
<option value="processing">处理中</option>
<option value="resolved">已解决</option>
<option value="closed">已关闭</option>
</select>
</div>
<div class="filter-item">
<label>VIP等级</label>
<select v-model="filters.vipLevel" @change="applyFilters">
<option value="">全部等级</option>
<option value="vip">VIP</option>
<option value="svip">SVIP</option>
</select>
</div>
<div class="filter-item">
<label>客户名称</label>
<input type="text" v-model="filters.customer" @input="applyFilters" placeholder="输入客户名称">
</div>
<div class="filter-item">
<label>优先级</label>
<select v-model="filters.priority" @change="applyFilters">
<option value="">全部优先级</option>
<option value="high"></option>
<option value="medium"></option>
<option value="low"></option>
</select>
</div>
</div>
<div class="ticket-list" v-if="filteredTickets.length > 0">
<div
v-for="ticket in filteredTickets"
:key="ticket.id"
class="ticket-item"
:class="ticket.vipLevel"
@click="viewTicket(ticket.id)"
>
<div class="ticket-header">
<div>
<span class="ticket-id">工单 #{{ ticket.id }}</span>
<span class="vip-badge" :class="ticket.vipLevel">
{{ ticket.vipLevel === 'vip' ? 'VIP' : 'SVIP' }}
</span>
<span class="priority-badge" :class="'priority-' + ticket.priority">
{{ ticket.priority === 'high' ? '高优先级' : (ticket.priority === 'medium' ? '中优先级' : '低优先级') }}
</span>
</div>
<span class="ticket-status" :class="'status-' + ticket.status">
{{ getStatusText(ticket.status) }}
</span>
</div>
<div class="ticket-info">
<div><strong>客户名称:</strong>{{ ticket.customer }}</div>
<div><strong>工单类型:</strong>{{ ticket.type }}</div>
<div><strong>责任人:</strong>{{ ticket.assignee }}</div>
<div><strong>创建时间:</strong>{{ formatTime(ticket.createTime) }}</div>
<div v-if="ticket.responseTime">
<strong>响应时间:</strong>{{ formatTime(ticket.responseTime) }}
</div>
</div>
<div class="sla-timer"
:class="{
warning: ticket.slaTimeRemaining < 3600000 && ticket.slaTimeRemaining > 0,
timeout: ticket.slaTimeRemaining <= 0
}">
⏱️ SLA剩余时间: {{ formatSlaTime(ticket.slaTimeRemaining) }}
</div>
<div class="ticket-actions" @click.stop>
<button
@click="viewTicket(ticket.id)"
class="btn btn-primary btn-sm"
>
查看详情
</button>
<button
v-if="ticket.status === 'pending'"
@click="confirmResponse(ticket.id)"
class="btn btn-sm"
style="background: #27ae60; color: white;"
>
确认响应
</button>
<button
v-if="ticket.status === 'processing'"
@click="resolveTicket(ticket.id)"
class="btn btn-sm"
style="background: #27ae60; color: white;"
>
问题已解决
</button>
</div>
</div>
</div>
<div class="empty-state" v-else>
<div class="empty-state-icon">🎫</div>
<div>暂无VIP服务工单</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
tickets: [],
filters: {
status: '',
vipLevel: '',
customer: '',
priority: ''
}
}
},
computed: {
stats() {
return {
total: this.tickets.length,
vip: this.tickets.filter(t => t.vipLevel === 'vip').length,
svip: this.tickets.filter(t => t.vipLevel === 'svip').length,
pending: this.tickets.filter(t => t.status === 'pending' || t.status === 'processing').length
};
},
filteredTickets() {
let result = [...this.tickets];
if (this.filters.status) {
result = result.filter(t => t.status === this.filters.status);
}
if (this.filters.vipLevel) {
result = result.filter(t => t.vipLevel === this.filters.vipLevel);
}
if (this.filters.customer) {
result = result.filter(t =>
t.customer.toLowerCase().includes(this.filters.customer.toLowerCase())
);
}
if (this.filters.priority) {
result = result.filter(t => t.priority === this.filters.priority);
}
return result.sort((a, b) => {
// SVIP优先,然后按优先级,最后按时间
if (a.vipLevel !== b.vipLevel) {
return a.vipLevel === 'svip' ? -1 : 1;
}
if (a.priority !== b.priority) {
const priorityOrder = { high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
}
return b.createTime - a.createTime;
});
}
},
mounted() {
this.loadTickets();
},
methods: {
loadTickets() {
// 从VIP客户列表获取VIP客户
const savedCustomers = localStorage.getItem('vipCustomers');
const vipCustomers = savedCustomers ? JSON.parse(savedCustomers).filter(c => c.level !== 'normal') : [];
// 生成VIP工单(示例数据)
if (vipCustomers.length > 0) {
this.tickets = [
{
id: 'VIP001',
customer: vipCustomers[0].name || '客户A',
vipLevel: vipCustomers[0].vipLevel || 'vip',
type: '技术支持',
status: 'processing',
priority: 'high',
assignee: '张三',
createTime: Date.now() - 2 * 60 * 60 * 1000,
responseTime: Date.now() - 1.5 * 60 * 60 * 1000,
slaTimeRemaining: 30 * 60 * 1000
},
{
id: 'SVIP001',
customer: vipCustomers.find(c => c.level === 'svip')?.name || '客户B',
vipLevel: 'svip',
type: '紧急故障',
status: 'pending',
priority: 'high',
assignee: '李四',
createTime: Date.now() - 30 * 60 * 1000,
slaTimeRemaining: 15 * 60 * 1000
}
];
}
},
applyFilters() {
// 筛选逻辑在computed中处理
},
viewTicket(ticketId) {
alert('查看工单详情: ' + ticketId);
// 这里可以跳转到工单详情页
},
confirmResponse(ticketId) {
const ticket = this.tickets.find(t => t.id === ticketId);
if (ticket) {
ticket.status = 'processing';
ticket.responseTime = Date.now();
alert('已确认响应,开始处理问题');
}
},
resolveTicket(ticketId) {
const ticket = this.tickets.find(t => t.id === ticketId);
if (ticket) {
const solution = prompt('请输入解决方案:');
if (solution) {
ticket.status = 'resolved';
ticket.resolveTime = Date.now();
ticket.solution = solution;
alert('问题已解决');
}
}
},
getStatusText(status) {
const statusMap = {
'pending': '待响应',
'processing': '处理中',
'resolved': '已解决',
'closed': '已关闭'
};
return statusMap[status] || status;
},
formatTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN');
},
formatSlaTime(remaining) {
if (remaining <= 0) return '已超时';
const hours = Math.floor(remaining / 3600000);
const minutes = Math.floor((remaining % 3600000) / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
return `${hours}小时 ${minutes}分钟 ${seconds}秒`;
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP SLA规则配置 - VIP客户服务机制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 30px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-size: 14px;
font-weight: 500;
}
.form-group label.required::after {
content: ' *';
color: #e74c3c;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-group input[type="number"] {
width: 200px;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: flex-end;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.help-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.sla-comparison {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.sla-comparison h3 {
margin-bottom: 15px;
color: #333;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
}
.comparison-table th,
.comparison-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.comparison-table th {
background: #667eea;
color: white;
font-weight: bold;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #27ae60;
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>⏱️ VIP SLA规则配置</h1>
<div class="nav-menu">
<a href="1-VIP客户管理列表页.html" class="nav-item">VIP客户管理</a>
<a href="3-VIP专属负责人配置页.html" class="nav-item">负责人配置</a>
<a href="4-VIP服务工单列表页.html" class="nav-item">VIP工单</a>
<a href="5-VIP SLA规则配置页.html" class="nav-item active">SLA配置</a>
<a href="6-VIP客户服务记录页.html" class="nav-item">服务记录</a>
<a href="7-VIP客户服务看板.html" class="nav-item">服务看板</a>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
SLA规则配置
</div>
<form @submit.prevent="saveSlaRules">
<div class="form-group">
<label class="required">客户类型</label>
<select v-model="formData.customerType" required @change="loadSlaRule">
<option value="normal">普通客户</option>
<option value="vip">VIP客户</option>
<option value="svip">SVIP客户</option>
</select>
<div class="help-text">选择要配置的客户类型</div>
</div>
<div class="form-group">
<label class="required">首次响应时间(分钟)</label>
<input type="number" v-model.number="formData.firstResponseTime" required min="1" placeholder="例如:15">
<div class="help-text">工单创建后,首次响应的时间限制(单位:分钟)</div>
</div>
<div class="form-group">
<label class="required">问题解决时限(小时)</label>
<input type="number" v-model.number="formData.resolveTimeLimit" required min="1" placeholder="例如:4">
<div class="help-text">从工单创建到问题解决的时间限制(单位:小时)</div>
</div>
<div class="form-group">
<label>是否启用SLA超时预警</label>
<label class="switch" style="margin-left: 10px;">
<input type="checkbox" v-model="formData.enableWarning">
<span class="slider"></span>
</label>
<div class="help-text">开启后,当SLA剩余时间不足时会发送预警通知</div>
</div>
<div class="form-group" v-if="formData.enableWarning">
<label>预警时间阈值(分钟)</label>
<input type="number" v-model.number="formData.warningThreshold" min="1" placeholder="例如:30">
<div class="help-text">当SLA剩余时间低于此值时触发预警</div>
</div>
<div class="form-actions">
<button type="button" @click="cancel" class="btn btn-secondary">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
<!-- SLA对比说明 -->
<div class="sla-comparison">
<h3>当前SLA规则对比</h3>
<table class="comparison-table">
<thead>
<tr>
<th>客户类型</th>
<th>首次响应时间</th>
<th>问题解决时限</th>
<th>预警功能</th>
</tr>
</thead>
<tbody>
<tr>
<td>普通客户</td>
<td>{{ slaRules.normal?.firstResponseTime || '-' }} 分钟</td>
<td>{{ slaRules.normal?.resolveTimeLimit || '-' }} 小时</td>
<td>{{ slaRules.normal?.enableWarning ? '已启用' : '未启用' }}</td>
</tr>
<tr>
<td>VIP客户</td>
<td>{{ slaRules.vip?.firstResponseTime || '-' }} 分钟</td>
<td>{{ slaRules.vip?.resolveTimeLimit || '-' }} 小时</td>
<td>{{ slaRules.vip?.enableWarning ? '已启用' : '未启用' }}</td>
</tr>
<tr>
<td>SVIP客户</td>
<td>{{ slaRules.svip?.firstResponseTime || '-' }} 分钟</td>
<td>{{ slaRules.svip?.resolveTimeLimit || '-' }} 小时</td>
<td>{{ slaRules.svip?.enableWarning ? '已启用' : '未启用' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
formData: {
customerType: 'normal',
firstResponseTime: 30,
resolveTimeLimit: 8,
enableWarning: true,
warningThreshold: 30
},
slaRules: {
normal: null,
vip: null,
svip: null
}
}
},
mounted() {
this.loadSlaRules();
},
methods: {
loadSlaRules() {
const saved = localStorage.getItem('vipSlaRules');
if (saved) {
this.slaRules = JSON.parse(saved);
// 加载当前类型的规则
this.loadSlaRule();
} else {
// 初始化默认规则
this.slaRules = {
normal: {
customerType: 'normal',
firstResponseTime: 30,
resolveTimeLimit: 8,
enableWarning: true,
warningThreshold: 30
},
vip: {
customerType: 'vip',
firstResponseTime: 15,
resolveTimeLimit: 4,
enableWarning: true,
warningThreshold: 15
},
svip: {
customerType: 'svip',
firstResponseTime: 5,
resolveTimeLimit: 2,
enableWarning: true,
warningThreshold: 10
}
};
this.saveSlaRules();
this.loadSlaRule();
}
},
loadSlaRule() {
const rule = this.slaRules[this.formData.customerType];
if (rule) {
this.formData = { ...rule };
} else {
// 默认值
this.formData = {
customerType: this.formData.customerType,
firstResponseTime: 30,
resolveTimeLimit: 8,
enableWarning: true,
warningThreshold: 30
};
}
},
saveSlaRules() {
const rule = {
...this.formData
};
this.slaRules[this.formData.customerType] = rule;
localStorage.setItem('vipSlaRules', JSON.stringify(this.slaRules));
alert('保存成功');
},
cancel() {
this.loadSlaRule();
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP客户服务记录 - VIP客户服务机制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.filter-bar {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 150px;
}
.filter-item label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
.filter-item select,
.filter-item input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.data-table th {
background: #f8f9fa;
font-weight: bold;
color: #333;
}
.data-table tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.badge-success {
background: #27ae60;
color: white;
}
.badge-warning {
background: #f39c12;
color: white;
}
.badge-danger {
background: #e74c3c;
color: white;
}
.badge-vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
}
.badge-svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 15px;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card.success {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>📋 VIP客户服务记录</h1>
<div class="nav-menu">
<a href="1-VIP客户管理列表页.html" class="nav-item">VIP客户管理</a>
<a href="3-VIP专属负责人配置页.html" class="nav-item">负责人配置</a>
<a href="4-VIP服务工单列表页.html" class="nav-item">VIP工单</a>
<a href="5-VIP SLA规则配置页.html" class="nav-item">SLA配置</a>
<a href="6-VIP客户服务记录页.html" class="nav-item active">服务记录</a>
<a href="7-VIP客户服务看板.html" class="nav-item">服务看板</a>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-bar">
<div class="stat-card">
<div class="stat-label">总服务次数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
<div class="stat-card success">
<div class="stat-label">已完成</div>
<div class="stat-value">{{ stats.completed }}</div>
</div>
<div class="stat-card warning">
<div class="stat-label">处理中</div>
<div class="stat-value">{{ stats.processing }}</div>
</div>
<div class="stat-card">
<div class="stat-label">本月服务</div>
<div class="stat-value">{{ stats.thisMonth }}</div>
</div>
</div>
<div class="card">
<div class="card-title">
VIP客户服务记录 (共 {{ filteredRecords.length }} 条)
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item">
<label>客户名称</label>
<input type="text" v-model="filters.customer" @input="applyFilters" placeholder="输入客户名称">
</div>
<div class="filter-item">
<label>服务类型</label>
<select v-model="filters.serviceType" @change="applyFilters">
<option value="">全部类型</option>
<option value="技术支持">技术支持</option>
<option value="故障处理">故障处理</option>
<option value="咨询解答">咨询解答</option>
<option value="维护服务">维护服务</option>
</select>
</div>
<div class="filter-item">
<label>服务状态</label>
<select v-model="filters.status" @change="applyFilters">
<option value="">全部状态</option>
<option value="completed">已完成</option>
<option value="processing">处理中</option>
<option value="pending">待处理</option>
</select>
</div>
<div class="filter-item">
<label>负责人</label>
<input type="text" v-model="filters.manager" @input="applyFilters" placeholder="输入负责人">
</div>
<div class="filter-item">
<label>时间范围</label>
<select v-model="filters.timeRange" @change="applyFilters">
<option value="all">全部时间</option>
<option value="today">今天</option>
<option value="week">最近7天</option>
<option value="month">最近30天</option>
</select>
</div>
</div>
<!-- 数据表格 -->
<table class="data-table" v-if="filteredRecords.length > 0">
<thead>
<tr>
<th>客户名称</th>
<th>VIP等级</th>
<th>服务时间</th>
<th>服务类型</th>
<th>问题描述</th>
<th>处理结果</th>
<th>负责人</th>
<th>服务状态</th>
</tr>
</thead>
<tbody>
<tr v-for="record in filteredRecords" :key="record.id">
<td>{{ record.customerName }}</td>
<td>
<span class="badge" :class="record.vipLevel === 'vip' ? 'badge-vip' : 'badge-svip'">
{{ record.vipLevel === 'vip' ? 'VIP' : 'SVIP' }}
</span>
</td>
<td>{{ formatTime(record.serviceTime) }}</td>
<td>{{ record.serviceType }}</td>
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ record.problemDescription }}
</td>
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ record.result || '-' }}
</td>
<td>{{ record.manager }}</td>
<td>
<span class="badge" :class="getStatusBadgeClass(record.status)">
{{ getStatusText(record.status) }}
</span>
</td>
</tr>
</tbody>
</table>
<div class="empty-state" v-else>
<div class="empty-state-icon">📋</div>
<div>暂无服务记录</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
records: [],
filters: {
customer: '',
serviceType: '',
status: '',
manager: '',
timeRange: 'all'
}
}
},
computed: {
stats() {
const now = new Date();
const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
return {
total: this.records.length,
completed: this.records.filter(r => r.status === 'completed').length,
processing: this.records.filter(r => r.status === 'processing').length,
thisMonth: this.records.filter(r => r.serviceTime >= thisMonthStart).length
};
},
filteredRecords() {
let result = [...this.records];
if (this.filters.customer) {
result = result.filter(r =>
r.customerName.toLowerCase().includes(this.filters.customer.toLowerCase())
);
}
if (this.filters.serviceType) {
result = result.filter(r => r.serviceType === this.filters.serviceType);
}
if (this.filters.status) {
result = result.filter(r => r.status === this.filters.status);
}
if (this.filters.manager) {
result = result.filter(r =>
r.manager.toLowerCase().includes(this.filters.manager.toLowerCase())
);
}
// 时间范围筛选
if (this.filters.timeRange === 'today') {
const today = new Date();
today.setHours(0, 0, 0, 0);
result = result.filter(r => r.serviceTime >= today.getTime());
} else if (this.filters.timeRange === 'week') {
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
result = result.filter(r => r.serviceTime >= weekAgo);
} else if (this.filters.timeRange === 'month') {
const monthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
result = result.filter(r => r.serviceTime >= monthAgo);
}
return result.sort((a, b) => b.serviceTime - a.serviceTime);
}
},
mounted() {
this.loadRecords();
},
methods: {
loadRecords() {
const saved = localStorage.getItem('vipServiceRecords');
if (saved) {
this.records = JSON.parse(saved);
} else {
// 初始化示例数据
this.records = [
{
id: 1,
customerName: '客户A',
vipLevel: 'vip',
serviceTime: Date.now() - 2 * 24 * 60 * 60 * 1000,
serviceType: '技术支持',
problemDescription: '系统运行缓慢,需要优化',
result: '已优化系统配置,性能提升30%',
manager: '张三',
status: 'completed'
},
{
id: 2,
customerName: '客户B',
vipLevel: 'svip',
serviceTime: Date.now() - 1 * 24 * 60 * 60 * 1000,
serviceType: '故障处理',
problemDescription: '服务器宕机,紧急处理',
result: '已恢复服务,问题已解决',
manager: '李四',
status: 'completed'
},
{
id: 3,
customerName: '客户A',
vipLevel: 'vip',
serviceTime: Date.now() - 5 * 60 * 60 * 1000,
serviceType: '咨询解答',
problemDescription: '关于新功能的使用咨询',
result: '',
manager: '王五',
status: 'processing'
}
];
this.saveRecords();
}
},
saveRecords() {
localStorage.setItem('vipServiceRecords', JSON.stringify(this.records));
},
applyFilters() {
// 筛选逻辑在computed中处理
},
getStatusText(status) {
const statusMap = {
'completed': '已完成',
'processing': '处理中',
'pending': '待处理'
};
return statusMap[status] || status;
},
getStatusBadgeClass(status) {
const classMap = {
'completed': 'badge-success',
'processing': 'badge-warning',
'pending': 'badge-danger'
};
return classMap[status] || '';
},
formatTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN');
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP客户服务看板 - VIP客户服务机制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 10px;
text-align: center;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card.vip {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
}
.stat-card.svip {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
}
.stat-card.success {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
}
.stat-card.danger {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
}
.stat-value {
font-size: 36px;
font-weight: bold;
margin: 15px 0;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
.stat-change {
font-size: 12px;
margin-top: 10px;
opacity: 0.8;
}
.chart-container {
position: relative;
height: 300px;
margin: 20px 0;
}
.chart-container.large {
height: 400px;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
@media (max-width: 768px) {
.grid-2,
.grid-3 {
grid-template-columns: 1fr;
}
}
.readonly-badge {
display: inline-block;
padding: 4px 10px;
background: #95a5a6;
color: white;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-left: 10px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>📊 VIP客户服务看板 <span class="readonly-badge">只读</span></h1>
<div class="nav-menu">
<a href="1-VIP客户管理列表页.html" class="nav-item">VIP客户管理</a>
<a href="3-VIP专属负责人配置页.html" class="nav-item">负责人配置</a>
<a href="4-VIP服务工单列表页.html" class="nav-item">VIP工单</a>
<a href="5-VIP SLA规则配置页.html" class="nav-item">SLA配置</a>
<a href="6-VIP客户服务记录页.html" class="nav-item">服务记录</a>
<a href="7-VIP客户服务看板.html" class="nav-item active">服务看板</a>
</div>
</div>
</div>
<!-- 核心指标统计 -->
<div class="stats-grid">
<div class="stat-card vip">
<div class="stat-label">当前VIP客户数量</div>
<div class="stat-value">{{ dashboardData.vipCount }}</div>
<div class="stat-change">VIP: {{ dashboardData.vipCount - dashboardData.svipCount }} | SVIP: {{ dashboardData.svipCount }}</div>
</div>
<div class="stat-card">
<div class="stat-label">本月VIP服务次数</div>
<div class="stat-value">{{ dashboardData.monthServiceCount }}</div>
<div class="stat-change">较上月 {{ dashboardData.monthChange >= 0 ? '+' : '' }}{{ dashboardData.monthChange }}%</div>
</div>
<div class="stat-card success">
<div class="stat-value">{{ dashboardData.slaComplianceRate }}%</div>
<div class="stat-label">VIP服务SLA达标率</div>
<div class="stat-change">目标: ≥95%</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ dashboardData.avgResponseTime }}</div>
<div class="stat-label">平均响应时间</div>
<div class="stat-change">分钟</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ dashboardData.avgResolveTime }}</div>
<div class="stat-label">平均解决时间</div>
<div class="stat-change">小时</div>
</div>
<div class="stat-card danger">
<div class="stat-value">{{ dashboardData.slaTimeoutCount }}</div>
<div class="stat-label">SLA超时次数</div>
<div class="stat-change">本月累计</div>
</div>
</div>
<!-- 图表区域 -->
<div class="grid-2">
<!-- VIP客户问题类型分布 -->
<div class="card">
<div class="card-title">VIP客户问题类型分布</div>
<div class="chart-container">
<canvas id="problemTypeChart"></canvas>
</div>
</div>
<!-- VIP服务趋势 -->
<div class="card">
<div class="card-title">VIP服务趋势(近30天)</div>
<div class="chart-container">
<canvas id="serviceTrendChart"></canvas>
</div>
</div>
</div>
<!-- VIP客户服务统计 -->
<div class="card">
<div class="card-title">VIP客户服务统计</div>
<div class="chart-container large">
<canvas id="customerServiceChart"></canvas>
</div>
</div>
<!-- SLA达标情况 -->
<div class="card">
<div class="card-title">SLA达标情况对比</div>
<div class="chart-container large">
<canvas id="slaComparisonChart"></canvas>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
dashboardData: {
vipCount: 0,
svipCount: 0,
monthServiceCount: 0,
monthChange: 0,
slaComplianceRate: 0,
avgResponseTime: 0,
avgResolveTime: 0,
slaTimeoutCount: 0
},
charts: {}
}
},
mounted() {
this.loadDashboardData();
this.$nextTick(() => {
this.initCharts();
});
},
methods: {
loadDashboardData() {
// 加载VIP客户数据
const savedCustomers = localStorage.getItem('vipCustomers');
const customers = savedCustomers ? JSON.parse(savedCustomers) : [];
this.dashboardData.vipCount = customers.filter(c => c.level !== 'normal').length;
this.dashboardData.svipCount = customers.filter(c => c.level === 'svip').length;
// 加载服务记录数据
const savedRecords = localStorage.getItem('vipServiceRecords');
const records = savedRecords ? JSON.parse(savedRecords) : [];
const now = new Date();
const thisMonthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1).getTime();
const lastMonthEnd = thisMonthStart;
const thisMonthRecords = records.filter(r => r.serviceTime >= thisMonthStart);
const lastMonthRecords = records.filter(r => r.serviceTime >= lastMonthStart && r.serviceTime < lastMonthEnd);
this.dashboardData.monthServiceCount = thisMonthRecords.length;
if (lastMonthRecords.length > 0) {
this.dashboardData.monthChange = Math.round(((thisMonthRecords.length - lastMonthRecords.length) / lastMonthRecords.length) * 100);
}
// 计算SLA达标率(示例数据)
const completedRecords = records.filter(r => r.status === 'completed');
const totalRecords = records.length;
if (totalRecords > 0) {
this.dashboardData.slaComplianceRate = Math.round((completedRecords.length / totalRecords) * 100);
}
// 计算平均响应时间和解决时间(示例数据)
this.dashboardData.avgResponseTime = 12;
this.dashboardData.avgResolveTime = 3.5;
this.dashboardData.slaTimeoutCount = 2;
},
initCharts() {
// 问题类型分布饼图
const problemTypeCtx = document.getElementById('problemTypeChart');
if (problemTypeCtx) {
this.charts.problemType = new Chart(problemTypeCtx, {
type: 'pie',
data: {
labels: ['技术支持', '故障处理', '咨询解答', '维护服务'],
datasets: [{
label: '问题类型',
data: [35, 25, 20, 20],
backgroundColor: [
'#667eea',
'#e74c3c',
'#f39c12',
'#27ae60'
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
},
tooltip: {
enabled: true,
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}
});
}
// 服务趋势折线图
const serviceTrendCtx = document.getElementById('serviceTrendChart');
if (serviceTrendCtx) {
this.charts.serviceTrend = new Chart(serviceTrendCtx, {
type: 'line',
data: {
labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
datasets: [{
label: 'VIP服务次数',
data: [12, 19, 15, 25, 22, 30],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 客户服务统计柱状图
const customerServiceCtx = document.getElementById('customerServiceChart');
if (customerServiceCtx) {
this.charts.customerService = new Chart(customerServiceCtx, {
type: 'bar',
data: {
labels: ['客户A', '客户B', '客户C', '客户D', '客户E'],
datasets: [{
label: '服务次数',
data: [45, 38, 32, 28, 22],
backgroundColor: [
'#f39c12',
'#e74c3c',
'#667eea',
'#27ae60',
'#3498db'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '服务次数'
}
},
x: {
title: {
display: true,
text: 'VIP客户'
}
}
}
}
});
}
// SLA达标情况对比
const slaComparisonCtx = document.getElementById('slaComparisonChart');
if (slaComparisonCtx) {
this.charts.slaComparison = new Chart(slaComparisonCtx, {
type: 'bar',
data: {
labels: ['普通客户', 'VIP客户', 'SVIP客户'],
datasets: [{
label: 'SLA达标率 (%)',
data: [85, 95, 98],
backgroundColor: [
'#95a5a6',
'#f39c12',
'#e74c3c'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: '达标率 (%)'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
}
}).mount('#app');
</script>
</body>
</html>
状态 条件 显示效果
++ /dev/null
状态 条件 显示效果
正常 剩余时间 > 1小时 黄色背景,正常显示
警告 0 < 剩余时间 ≤ 1小时 橙色/红色背景,警告提示
超时 剩余时间 ≤ 0 红色背景,闪烁动画
方案1:新开停机问题处理模块(列表页,工单页,详情页,统计页)
方案2:延用产品工单模块功能,增加(SLA计时字段,类型增加:停机/紧急 类型,停机/紧急类型自动添加SLA计时时间)
当前业务流程: 客户电话联系服务台客服------>客服内部联系工程师----------->工程师处理问题(电话&邮件&现场)-------------->问题解决(工单完结)
\ No newline at end of file
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>停机问题列表 - 停机问题处理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-bar {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 150px;
}
.filter-item label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
.filter-item select,
.filter-item input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.btn-sm {
padding: 6px 12px;
font-size: 14px;
}
.ticket-list {
max-height: 700px;
overflow-y: auto;
}
.ticket-item {
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s;
background: #f9f9f9;
cursor: pointer;
}
.ticket-item:hover {
border-color: #667eea;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.ticket-item.urgent {
border-color: #e74c3c;
background: #ffeaea;
}
.ticket-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.ticket-id {
font-weight: bold;
color: #667eea;
font-size: 16px;
}
.ticket-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.status-pending {
background: #f39c12;
color: white;
}
.status-processing {
background: #3498db;
color: white;
}
.status-resolved {
background: #27ae60;
color: white;
}
.status-closed {
background: #95a5a6;
color: white;
}
.priority-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-left: 10px;
}
.priority-urgent {
background: #e74c3c;
color: white;
}
.priority-downtime {
background: #c0392b;
color: white;
}
.ticket-info {
margin: 10px 0;
color: #666;
font-size: 14px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
}
.ticket-info strong {
color: #333;
}
.sla-timer {
display: inline-block;
padding: 6px 12px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
font-weight: bold;
color: #856404;
margin-top: 10px;
}
.sla-timer.warning {
background: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.sla-timer.timeout {
background: #721c24;
border-color: #dc3545;
color: white;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.ticket-actions {
margin-top: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 15px;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card.danger {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
}
.stat-card.success {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>🛑 停机问题列表</h1>
<div class="nav-menu">
<a href="1-停机问题列表页.html" class="nav-item active">工单列表</a>
<a href="2-创建停机工单页.html" class="nav-item">创建工单</a>
<a href="4-统计&分析页.html" class="nav-item">统计分析</a>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-bar">
<div class="stat-card danger">
<div class="stat-label">待响应</div>
<div class="stat-value">{{ stats.pending }}</div>
</div>
<div class="stat-card warning">
<div class="stat-label">处理中</div>
<div class="stat-value">{{ stats.processing }}</div>
</div>
<div class="stat-card">
<div class="stat-label">已解决</div>
<div class="stat-value">{{ stats.resolved }}</div>
</div>
<div class="stat-card success">
<div class="stat-label">已关闭</div>
<div class="stat-value">{{ stats.closed }}</div>
</div>
<div class="stat-card danger">
<div class="stat-label">SLA超时</div>
<div class="stat-value">{{ stats.timeout }}</div>
</div>
</div>
<div class="card">
<div class="card-title">
<span>停机工单列表 (共 {{ filteredTickets.length }} 条)</span>
<a href="2-创建停机工单页.html" class="btn btn-danger">
🚨 创建紧急工单
</a>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item">
<label>状态筛选</label>
<select v-model="filters.status" @change="applyFilters">
<option value="">全部状态</option>
<option value="pending">待响应</option>
<option value="processing">处理中</option>
<option value="resolved">已解决</option>
<option value="closed">已关闭</option>
</select>
</div>
<div class="filter-item">
<label>客户筛选</label>
<input type="text" v-model="filters.customer" @input="applyFilters" placeholder="输入客户名称">
</div>
<div class="filter-item">
<label>责任人筛选</label>
<input type="text" v-model="filters.assignee" @input="applyFilters" placeholder="输入责任人">
</div>
<div class="filter-item">
<label>排序方式</label>
<select v-model="filters.sortBy" @change="applyFilters">
<option value="time">按时间排序</option>
<option value="priority">按优先级排序</option>
<option value="sla">按SLA剩余时间排序</option>
</select>
</div>
</div>
<div class="ticket-list" v-if="filteredTickets.length > 0">
<div
v-for="ticket in filteredTickets"
:key="ticket.id"
class="ticket-item"
:class="{urgent: ticket.priority === 'downtime'}"
@click="viewTicket(ticket.id)"
>
<div class="ticket-header">
<div>
<span class="ticket-id">工单 #{{ ticket.id }}</span>
<span class="priority-badge" :class="'priority-' + ticket.priority">
{{ ticket.priority === 'downtime' ? '停机/紧急' : '紧急' }}
</span>
</div>
<span class="ticket-status" :class="'status-' + ticket.status">
{{ getStatusText(ticket.status) }}
</span>
</div>
<div class="ticket-info">
<div><strong>客户:</strong>{{ ticket.customer }}</div>
<div><strong>联系人:</strong>{{ ticket.contact }} ({{ ticket.phone }})</div>
<div><strong>设备/产线:</strong>{{ ticket.equipment }}</div>
<div><strong>责任人:</strong>{{ ticket.assignee }}</div>
<div><strong>创建时间:</strong>{{ formatTime(ticket.createTime) }}</div>
<div v-if="ticket.responseTime">
<strong>响应时间:</strong>{{ formatTime(ticket.responseTime) }}
</div>
<div v-if="ticket.resolveTime">
<strong>解决时间:</strong>{{ formatTime(ticket.resolveTime) }}
</div>
</div>
<div class="sla-timer"
:class="{
warning: ticket.slaTimeRemaining < 3600000 && ticket.slaTimeRemaining > 0,
timeout: ticket.slaTimeRemaining <= 0
}">
⏱️ SLA计时: {{ formatSlaTime(ticket.slaTimeRemaining) }}
</div>
<div class="ticket-actions" @click.stop>
<button
v-if="ticket.status === 'pending'"
@click="confirmResponse(ticket.id)"
class="btn btn-success btn-sm"
>
确认响应
</button>
<button
v-if="ticket.status === 'processing'"
@click="resolveTicket(ticket.id)"
class="btn btn-success btn-sm"
>
问题已解决
</button>
<button
@click="viewTicket(ticket.id)"
class="btn btn-primary btn-sm"
>
查看详情
</button>
<button
v-if="ticket.status === 'resolved'"
@click="closeTicket(ticket.id)"
class="btn btn-sm"
style="background: #95a5a6; color: white;"
>
关闭工单
</button>
</div>
</div>
</div>
<div class="empty-state" v-else>
<div class="empty-state-icon">📋</div>
<div>暂无符合条件的停机工单</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
tickets: [],
filters: {
status: '',
customer: '',
assignee: '',
sortBy: 'time'
},
slaLimit: 2 * 60 * 60 * 1000,
timer: null
}
},
computed: {
stats() {
const stats = {
pending: 0,
processing: 0,
resolved: 0,
closed: 0,
timeout: 0
};
this.tickets.forEach(ticket => {
if (ticket.status === 'pending') stats.pending++;
else if (ticket.status === 'processing') stats.processing++;
else if (ticket.status === 'resolved') stats.resolved++;
else if (ticket.status === 'closed') stats.closed++;
if (ticket.slaTimeRemaining <= 0 && ticket.status !== 'closed') stats.timeout++;
});
return stats;
},
filteredTickets() {
let result = [...this.tickets];
// 状态筛选
if (this.filters.status) {
result = result.filter(t => t.status === this.filters.status);
}
// 客户筛选
if (this.filters.customer) {
result = result.filter(t =>
t.customer.toLowerCase().includes(this.filters.customer.toLowerCase())
);
}
// 责任人筛选
if (this.filters.assignee) {
result = result.filter(t =>
t.assignee.toLowerCase().includes(this.filters.assignee.toLowerCase())
);
}
// 排序
if (this.filters.sortBy === 'time') {
result.sort((a, b) => b.createTime - a.createTime);
} else if (this.filters.sortBy === 'priority') {
result.sort((a, b) => {
if (a.priority !== b.priority) {
return a.priority === 'downtime' ? -1 : 1;
}
return b.createTime - a.createTime;
});
} else if (this.filters.sortBy === 'sla') {
result.sort((a, b) => a.slaTimeRemaining - b.slaTimeRemaining);
}
return result;
}
},
mounted() {
this.loadTickets();
this.startTimer();
},
beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
loadTickets() {
const saved = localStorage.getItem('downtimeTickets');
if (saved) {
this.tickets = JSON.parse(saved);
this.tickets.forEach(ticket => {
if (ticket.status !== 'closed' && ticket.slaStartTime) {
const elapsed = Date.now() - ticket.slaStartTime;
ticket.slaTimeRemaining = Math.max(0, this.slaLimit - elapsed);
}
});
}
},
startTimer() {
this.timer = setInterval(() => {
this.tickets.forEach(ticket => {
if (ticket.status !== 'closed' && ticket.slaStartTime) {
const elapsed = Date.now() - ticket.slaStartTime;
ticket.slaTimeRemaining = Math.max(0, this.slaLimit - elapsed);
}
});
this.saveTickets();
}, 1000);
},
saveTickets() {
localStorage.setItem('downtimeTickets', JSON.stringify(this.tickets));
},
applyFilters() {
// 筛选逻辑在computed中处理
},
viewTicket(ticketId) {
window.location.href = `3-停机工单详情页.html?id=${ticketId}`;
},
confirmResponse(ticketId) {
const ticket = this.tickets.find(t => t.id === ticketId);
if (ticket) {
ticket.status = 'processing';
ticket.responseTime = Date.now();
this.saveTickets();
alert('已确认响应,开始处理问题');
}
},
resolveTicket(ticketId) {
const ticket = this.tickets.find(t => t.id === ticketId);
if (ticket) {
const solution = prompt('请输入解决方案:');
if (solution) {
ticket.status = 'resolved';
ticket.resolveTime = Date.now();
ticket.solution = solution;
this.saveTickets();
alert('问题已解决,等待关闭工单');
}
}
},
closeTicket(ticketId) {
const ticket = this.tickets.find(t => t.id === ticketId);
if (ticket) {
const summary = prompt('请输入复盘总结:');
if (summary !== null) {
ticket.status = 'closed';
ticket.closeTime = Date.now();
ticket.summary = summary || '无';
this.saveTickets();
alert('工单已关闭');
}
}
},
getStatusText(status) {
const statusMap = {
'pending': '待响应',
'processing': '处理中',
'resolved': '已解决',
'closed': '已关闭'
};
return statusMap[status] || status;
},
formatTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN');
},
formatSlaTime(remaining) {
if (remaining <= 0) return '已超时';
const hours = Math.floor(remaining / 3600000);
const minutes = Math.floor((remaining % 3600000) / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
return `${hours}小时 ${minutes}分钟 ${seconds}秒`;
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>创建停机工单 - 停机问题处理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.card-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 3px solid #e74c3c;
display: flex;
align-items: center;
gap: 10px;
}
.alert-box {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 15px;
margin-bottom: 25px;
color: #856404;
}
.alert-box strong {
color: #e74c3c;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 15px;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
resize: vertical;
min-height: 120px;
}
.required {
color: #e74c3c;
font-weight: bold;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.btn {
padding: 15px 30px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-danger {
background: #e74c3c;
color: white;
font-size: 18px;
padding: 18px 40px;
width: 100%;
margin-top: 20px;
}
.btn-danger:hover {
background: #c0392b;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.4);
}
.btn-secondary {
background: #95a5a6;
color: white;
margin-right: 10px;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.flow-diagram {
background: #f8f9fa;
padding: 25px;
border-radius: 8px;
margin-top: 30px;
}
.flow-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.flow-step {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 12px;
background: white;
border-radius: 6px;
border-left: 4px solid #667eea;
}
.flow-step-number {
width: 30px;
height: 30px;
background: #667eea;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 15px;
flex-shrink: 0;
}
.notification-badge {
position: fixed;
top: 20px;
right: 20px;
background: #e74c3c;
color: white;
padding: 15px 25px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 999;
animation: slideIn 0.3s;
font-size: 16px;
}
@keyframes slideIn {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>🛑 创建停机工单(特殊通道)</h1>
<div class="nav-menu">
<a href="1-停机问题列表页.html" class="nav-item">工单列表</a>
<a href="2-创建停机工单页.html" class="nav-item active">创建工单</a>
<a href="4-统计&分析页.html" class="nav-item">统计分析</a>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
<span>🚨 紧急停机工单创建</span>
</div>
<div class="alert-box">
<strong>⚠️ 重要提示:</strong>此通道专为24小时连续生产客户的停机问题设计。
提交后将自动标记为【停机/紧急】级别,立即开始SLA计时(2小时),并自动通知相关责任人。
</div>
<form @submit.prevent="createTicket">
<div class="form-row">
<div class="form-group">
<label>客户名称 <span class="required">*</span></label>
<input type="text" v-model="newTicket.customer" required placeholder="请输入客户名称">
</div>
<div class="form-group">
<label>联系人 <span class="required">*</span></label>
<input type="text" v-model="newTicket.contact" required placeholder="请输入联系人">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>联系电话 <span class="required">*</span></label>
<input type="tel" v-model="newTicket.phone" required placeholder="请输入联系电话">
</div>
<div class="form-group">
<label>发现方式 <span class="required">*</span></label>
<select v-model="newTicket.discoveryMethod" required>
<option value="">请选择</option>
<option value="customer">客户发现</option>
<option value="internal">内部发现</option>
</select>
</div>
</div>
<div class="form-group">
<label>停机设备/产线 <span class="required">*</span></label>
<input type="text" v-model="newTicket.equipment" required placeholder="请输入停机设备或产线名称">
</div>
<div class="form-group">
<label>问题描述 <span class="required">*</span></label>
<textarea v-model="newTicket.description" required placeholder="请详细描述停机问题,包括故障现象、影响范围等"></textarea>
</div>
<div class="form-group">
<label>责任人 <span class="required">*</span></label>
<input type="text" v-model="newTicket.assignee" required placeholder="请输入责任人姓名">
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button type="button" @click="resetForm" class="btn btn-danger btn-secondary" style=" white-space: nowrap;width: 200px;">
重置表单
</button>
<button type="submit" class="btn btn-danger">
🚨 提交停机工单(紧急)
</button>
</div>
</form>
<!-- 处理流程说明 -->
<div class="flow-diagram">
<div class="flow-title">📋 停机问题处理流程</div>
<div class="flow-step" v-for="(step, index) in processFlow" :key="index">
<div class="flow-step-number">{{ index + 1 }}</div>
<div>{{ step }}</div>
</div>
</div>
</div>
</div>
<!-- 通知提示 -->
<div class="notification-badge" v-if="notification.show">
{{ notification.message }}
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
newTicket: {
customer: '',
contact: '',
phone: '',
equipment: '',
description: '',
discoveryMethod: '',
assignee: ''
},
notification: {
show: false,
message: ''
},
processFlow: [
'客户/内部发现停机',
'提交停机问题工单(特殊通道)',
'系统自动标记停机级别',
'自动通知(短信/电话/IM)',
'责任人确认 & 响应(SLA计时开始)',
'问题处理 / 临时解决方案',
'恢复生产',
'关闭工单 & 复盘总结'
],
slaLimit: 2 * 60 * 60 * 1000 // 2小时SLA限制
}
},
methods: {
createTicket() {
const ticket = {
id: 'DT' + Date.now(),
customer: this.newTicket.customer,
contact: this.newTicket.contact,
phone: this.newTicket.phone,
equipment: this.newTicket.equipment,
description: this.newTicket.description,
discoveryMethod: this.newTicket.discoveryMethod,
assignee: this.newTicket.assignee,
priority: 'downtime', // 自动标记为停机/紧急
status: 'pending',
createTime: Date.now(),
responseTime: null,
resolveTime: null,
closeTime: null,
solution: '',
summary: '',
slaStartTime: Date.now(), // SLA计时开始
slaTimeRemaining: this.slaLimit
};
// 保存到localStorage
let tickets = [];
const saved = localStorage.getItem('downtimeTickets');
if (saved) {
tickets = JSON.parse(saved);
}
tickets.unshift(ticket);
localStorage.setItem('downtimeTickets', JSON.stringify(tickets));
// 发送通知
this.sendNotification(ticket);
// 显示成功消息
this.showNotification('停机工单已创建!系统已自动标记为紧急,SLA计时已开始');
// 重置表单
this.resetForm();
// 延迟跳转到列表页
setTimeout(() => {
if (confirm('工单创建成功!是否跳转到工单列表?')) {
window.location.href = '1-停机问题列表页.html';
}
}, 2000);
},
resetForm() {
this.newTicket = {
customer: '',
contact: '',
phone: '',
equipment: '',
description: '',
discoveryMethod: '',
assignee: ''
};
},
sendNotification(ticket) {
// 模拟发送通知(短信/电话/IM)
console.log('发送通知:', {
短信: `【停机警报】工单${ticket.id}已创建,客户:${ticket.customer},责任人:${ticket.assignee}`,
电话: `紧急通知:${ticket.assignee},请立即处理停机工单${ticket.id}`,
IM: `@${ticket.assignee} 新的停机工单 #${ticket.id} 需要您立即处理`
});
},
showNotification(message) {
this.notification.message = message;
this.notification.show = true;
setTimeout(() => {
this.notification.show = false;
}, 5000);
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工单详情 - 停机问题处理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.card {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.ticket-header-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.info-item {
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.info-item strong {
display: block;
color: #667eea;
margin-bottom: 8px;
font-size: 14px;
}
.info-item span {
color: #333;
font-size: 16px;
}
.priority-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
}
.priority-downtime {
background: #c0392b;
color: white;
}
.ticket-status {
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
}
.status-pending {
background: #f39c12;
color: white;
}
.status-processing {
background: #3498db;
color: white;
}
.status-resolved {
background: #27ae60;
color: white;
}
.status-closed {
background: #95a5a6;
color: white;
}
.sla-timer {
display: inline-block;
padding: 10px 20px;
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 6px;
font-weight: bold;
color: #856404;
font-size: 18px;
margin: 20px 0;
}
.sla-timer.warning {
background: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.sla-timer.timeout {
background: #721c24;
border-color: #dc3545;
color: white;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.detail-section {
margin-bottom: 25px;
}
.detail-section h3 {
color: #333;
margin-bottom: 15px;
font-size: 18px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.detail-content {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
color: #555;
line-height: 1.8;
white-space: pre-wrap;
}
.timeline {
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: #e0e0e0;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
padding-left: 20px;
}
.timeline-item::before {
content: '';
position: absolute;
left: -25px;
top: 5px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #667eea;
border: 2px solid white;
box-shadow: 0 0 0 2px #667eea;
}
.timeline-item.completed::before {
background: #27ae60;
box-shadow: 0 0 0 2px #27ae60;
}
.timeline-content {
background: #f8f9fa;
padding: 12px 15px;
border-radius: 6px;
}
.timeline-title {
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.timeline-time {
color: #999;
font-size: 12px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
font-weight: 500;
margin-right: 10px;
margin-bottom: 10px;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 15px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>🛑 停机工单详情</h1>
<div class="nav-menu">
<a href="1-停机问题列表页.html" class="nav-item">工单列表</a>
<a href="2-创建停机工单页.html" class="nav-item">创建工单</a>
<a href="4-统计&分析页.html" class="nav-item">统计分析</a>
</div>
</div>
</div>
<div v-if="ticket.id">
<!-- 工单基本信息 -->
<div class="card">
<div class="card-title">工单基本信息</div>
<div class="ticket-header-info">
<div class="info-item">
<strong>工单编号</strong>
<span>{{ ticket.id }}</span>
</div>
<div class="info-item">
<strong>优先级</strong>
<span class="priority-badge priority-downtime">停机/紧急</span>
</div>
<div class="info-item">
<strong>当前状态</strong>
<span class="ticket-status" :class="'status-' + ticket.status">
{{ getStatusText(ticket.status) }}
</span>
</div>
</div>
<div class="sla-timer"
:class="{
warning: ticket.slaTimeRemaining < 3600000 && ticket.slaTimeRemaining > 0,
timeout: ticket.slaTimeRemaining <= 0
}">
⏱️ SLA计时: {{ formatSlaTime(ticket.slaTimeRemaining) }}
</div>
<div class="ticket-header-info">
<div class="info-item">
<strong>客户名称</strong>
<span>{{ ticket.customer }}</span>
</div>
<div class="info-item">
<strong>联系人</strong>
<span>{{ ticket.contact }}</span>
</div>
<div class="info-item">
<strong>联系电话</strong>
<span>{{ ticket.phone }}</span>
</div>
<div class="info-item">
<strong>停机设备/产线</strong>
<span>{{ ticket.equipment }}</span>
</div>
<div class="info-item">
<strong>发现方式</strong>
<span>{{ ticket.discoveryMethod === 'customer' ? '客户发现' : '内部发现' }}</span>
</div>
<div class="info-item">
<strong>责任人</strong>
<span>{{ ticket.assignee }}</span>
</div>
</div>
</div>
<!-- 问题描述 -->
<div class="card">
<div class="card-title">问题描述</div>
<div class="detail-section">
<div class="detail-content">{{ ticket.description }}</div>
</div>
</div>
<!-- 处理流程时间线 -->
<div class="card">
<div class="card-title">处理流程时间线</div>
<div class="timeline">
<div class="timeline-item" :class="{completed: ticket.createTime}">
<div class="timeline-content">
<div class="timeline-title">工单创建</div>
<div class="timeline-time">{{ formatTime(ticket.createTime) }}</div>
</div>
</div>
<div class="timeline-item" :class="{completed: ticket.responseTime}">
<div class="timeline-content">
<div class="timeline-title">责任人响应确认</div>
<div class="timeline-time" v-if="ticket.responseTime">{{ formatTime(ticket.responseTime) }}</div>
<div class="timeline-time" v-else>待响应</div>
</div>
</div>
<div class="timeline-item" :class="{completed: ticket.resolveTime}">
<div class="timeline-content">
<div class="timeline-title">问题解决</div>
<div class="timeline-time" v-if="ticket.resolveTime">{{ formatTime(ticket.resolveTime) }}</div>
<div class="timeline-time" v-else>处理中</div>
</div>
</div>
<div class="timeline-item" :class="{completed: ticket.closeTime}">
<div class="timeline-content">
<div class="timeline-title">工单关闭</div>
<div class="timeline-time" v-if="ticket.closeTime">{{ formatTime(ticket.closeTime) }}</div>
<div class="timeline-time" v-else>未关闭</div>
</div>
</div>
</div>
</div>
<!-- 解决方案 -->
<div class="card" v-if="ticket.solution">
<div class="card-title">解决方案</div>
<div class="detail-section">
<div class="detail-content">{{ ticket.solution }}</div>
</div>
</div>
<!-- 复盘总结 -->
<div class="card" v-if="ticket.summary">
<div class="card-title">复盘总结</div>
<div class="detail-section">
<div class="detail-content">{{ ticket.summary }}</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="card">
<div class="card-title">操作</div>
<div>
<button
v-if="ticket.status === 'pending'"
@click="confirmResponse"
class="btn btn-success"
>
确认响应
</button>
<button
v-if="ticket.status === 'processing'"
@click="resolveTicket"
class="btn btn-success"
>
问题已解决
</button>
<button
v-if="ticket.status === 'resolved'"
@click="closeTicket"
class="btn btn-success"
>
关闭工单
</button>
<a href="1-停机问题列表页.html" class="btn btn-primary">
返回列表
</a>
</div>
</div>
</div>
<div class="card" v-else>
<div class="empty-state">
<div class="empty-state-icon"></div>
<div>未找到工单信息</div>
<a href="1-停机问题列表页.html" class="btn btn-primary" style="margin-top: 20px; display: inline-block;">
返回列表
</a>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
ticket: {},
ticketId: null,
slaLimit: 2 * 60 * 60 * 1000,
timer: null
}
},
mounted() {
// 从URL获取工单ID
const urlParams = new URLSearchParams(window.location.search);
this.ticketId = urlParams.get('id');
this.loadTicket();
if (this.ticket.id && this.ticket.status !== 'closed') {
this.startTimer();
}
},
beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
loadTicket() {
const saved = localStorage.getItem('downtimeTickets');
if (saved && this.ticketId) {
const tickets = JSON.parse(saved);
this.ticket = tickets.find(t => t.id === this.ticketId) || {};
if (this.ticket.slaStartTime && this.ticket.status !== 'closed') {
const elapsed = Date.now() - this.ticket.slaStartTime;
this.ticket.slaTimeRemaining = Math.max(0, this.slaLimit - elapsed);
}
}
},
startTimer() {
this.timer = setInterval(() => {
if (this.ticket.status !== 'closed' && this.ticket.slaStartTime) {
const elapsed = Date.now() - this.ticket.slaStartTime;
this.ticket.slaTimeRemaining = Math.max(0, this.slaLimit - elapsed);
}
}, 1000);
},
saveTickets() {
const saved = localStorage.getItem('downtimeTickets');
if (saved) {
const tickets = JSON.parse(saved);
const index = tickets.findIndex(t => t.id === this.ticket.id);
if (index !== -1) {
tickets[index] = this.ticket;
localStorage.setItem('downtimeTickets', JSON.stringify(tickets));
}
}
},
confirmResponse() {
this.ticket.status = 'processing';
this.ticket.responseTime = Date.now();
this.saveTickets();
alert('已确认响应,开始处理问题');
},
resolveTicket() {
const solution = prompt('请输入解决方案:');
if (solution) {
this.ticket.status = 'resolved';
this.ticket.resolveTime = Date.now();
this.ticket.solution = solution;
this.saveTickets();
alert('问题已解决,等待关闭工单');
}
},
closeTicket() {
const summary = prompt('请输入复盘总结:');
if (summary !== null) {
this.ticket.status = 'closed';
this.ticket.closeTime = Date.now();
this.ticket.summary = summary || '无';
this.saveTickets();
alert('工单已关闭');
}
},
getStatusText(status) {
const statusMap = {
'pending': '待响应',
'processing': '处理中',
'resolved': '已解决',
'closed': '已关闭'
};
return statusMap[status] || status;
},
formatTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN');
},
formatSlaTime(remaining) {
if (remaining <= 0) return '已超时';
const hours = Math.floor(remaining / 3600000);
const minutes = Math.floor((remaining % 3600000) / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
return `${hours}小时 ${minutes}分钟 ${seconds}秒`;
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>统计&分析 - 停机问题处理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 28px;
}
.nav-menu {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-item {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.nav-item.active {
background: #e74c3c;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 10px;
text-align: center;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card.danger {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
}
.stat-card.success {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
}
.stat-card.info {
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
}
.stat-value {
font-size: 36px;
font-weight: bold;
margin: 15px 0;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
.stat-change {
font-size: 12px;
margin-top: 10px;
opacity: 0.8;
}
.chart-container {
position: relative;
height: 300px;
margin: 20px 0;
}
.chart-container.large {
height: 400px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.data-table th {
background: #f8f9fa;
font-weight: bold;
color: #333;
}
.data-table tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.badge-success {
background: #27ae60;
color: white;
}
.badge-warning {
background: #f39c12;
color: white;
}
.badge-danger {
background: #e74c3c;
color: white;
}
.filter-bar {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 150px;
}
.filter-item label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
.filter-item select,
.filter-item input {
width: 100%;
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
@media (max-width: 768px) {
.grid-2,
.grid-3 {
grid-template-columns: 1fr;
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 15px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>📊 统计&分析</h1>
<div class="nav-menu">
<a href="1-停机问题列表页.html" class="nav-item">工单列表</a>
<a href="2-创建停机工单页.html" class="nav-item">创建工单</a>
<a href="4-统计&分析页.html" class="nav-item active">统计分析</a>
</div>
</div>
</div>
<!-- 核心指标统计 -->
<div class="stats-grid">
<div class="stat-card danger">
<div class="stat-label">总工单数</div>
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-change">全部停机工单</div>
</div>
<div class="stat-card warning">
<div class="stat-label">待处理</div>
<div class="stat-value">{{ stats.pending }}</div>
<div class="stat-change">待响应 + 处理中</div>
</div>
<div class="stat-card success">
<div class="stat-value">{{ stats.resolvedRate }}%</div>
<div class="stat-label">解决率</div>
<div class="stat-change">已解决 / 总工单</div>
</div>
<div class="stat-card info">
<div class="stat-value">{{ stats.avgResponseTime }}</div>
<div class="stat-label">平均响应时间</div>
<div class="stat-change">分钟</div>
</div>
<div class="stat-card danger">
<div class="stat-value">{{ stats.avgResolveTime }}</div>
<div class="stat-label">平均解决时间</div>
<div class="stat-change">分钟</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ stats.timeoutCount }}</div>
<div class="stat-label">SLA超时数</div>
<div class="stat-change">当前超时工单</div>
</div>
</div>
<!-- 时间筛选 -->
<div class="card">
<div class="filter-bar">
<div class="filter-item">
<label>时间范围</label>
<select v-model="timeRange" @change="updateStats">
<option value="all">全部时间</option>
<option value="today">今天</option>
<option value="week">最近7天</option>
<option value="month">最近30天</option>
<option value="quarter">最近3个月</option>
</select>
</div>
<div class="filter-item">
<label>开始日期</label>
<input type="date" v-model="startDate" @change="updateStats">
</div>
<div class="filter-item">
<label>结束日期</label>
<input type="date" v-model="endDate" @change="updateStats">
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="grid-2">
<!-- 工单状态分布 -->
<div class="card">
<div class="card-title">工单状态分布</div>
<div class="chart-container">
<canvas id="statusChart"></canvas>
</div>
</div>
<!-- 工单趋势 -->
<div class="card">
<div class="card-title">工单创建趋势</div>
<div class="chart-container">
<canvas id="trendChart"></canvas>
</div>
</div>
</div>
<!-- 响应时间分析 -->
<div class="card">
<div class="card-title">响应时间分析</div>
<div class="chart-container large">
<canvas id="responseTimeChart"></canvas>
</div>
</div>
<!-- 责任人统计 -->
<div class="card">
<div class="card-title">责任人处理统计</div>
<div class="chart-container large">
<canvas id="assigneeChart"></canvas>
</div>
</div>
<!-- 客户统计 -->
<div class="card">
<div class="card-title">客户停机统计</div>
<table class="data-table">
<thead>
<tr>
<th>客户名称</th>
<th>工单数量</th>
<th>平均响应时间</th>
<th>平均解决时间</th>
<th>超时次数</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in customerStats" :key="index">
<td>{{ item.customer }}</td>
<td>{{ item.count }}</td>
<td>{{ item.avgResponse }} 分钟</td>
<td>{{ item.avgResolve }} 分钟</td>
<td>
<span class="badge" :class="item.timeout > 0 ? 'badge-danger' : 'badge-success'">
{{ item.timeout }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 详细数据表格 -->
<div class="card">
<div class="card-title">工单详细数据</div>
<table class="data-table">
<thead>
<tr>
<th>工单编号</th>
<th>客户</th>
<th>责任人</th>
<th>状态</th>
<th>创建时间</th>
<th>响应时间</th>
<th>解决时间</th>
<th>SLA状态</th>
</tr>
</thead>
<tbody>
<tr v-for="ticket in filteredTickets" :key="ticket.id">
<td>
<a :href="'3-停机工单详情页.html?id=' + ticket.id" style="color: #667eea; text-decoration: none;">
{{ ticket.id }}
</a>
</td>
<td>{{ ticket.customer }}</td>
<td>{{ ticket.assignee }}</td>
<td>
<span class="badge" :class="getStatusBadgeClass(ticket.status)">
{{ getStatusText(ticket.status) }}
</span>
</td>
<td>{{ formatTime(ticket.createTime) }}</td>
<td>{{ ticket.responseTime ? formatTime(ticket.responseTime) : '-' }}</td>
<td>{{ ticket.resolveTime ? formatTime(ticket.resolveTime) : '-' }}</td>
<td>
<span class="badge" :class="ticket.slaTimeRemaining <= 0 ? 'badge-danger' : 'badge-success'">
{{ ticket.slaTimeRemaining <= 0 ? '超时' : '正常' }}
</span>
</td>
</tr>
</tbody>
</table>
<div class="empty-state" v-if="filteredTickets.length === 0">
<div class="empty-state-icon">📊</div>
<div>暂无数据</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
tickets: [],
timeRange: 'all',
startDate: '',
endDate: '',
charts: {}
}
},
computed: {
filteredTickets() {
let result = [...this.tickets];
// 时间筛选
if (this.timeRange === 'today') {
const today = new Date();
today.setHours(0, 0, 0, 0);
result = result.filter(t => t.createTime >= today.getTime());
} else if (this.timeRange === 'week') {
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
result = result.filter(t => t.createTime >= weekAgo);
} else if (this.timeRange === 'month') {
const monthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
result = result.filter(t => t.createTime >= monthAgo);
} else if (this.timeRange === 'quarter') {
const quarterAgo = Date.now() - 90 * 24 * 60 * 60 * 1000;
result = result.filter(t => t.createTime >= quarterAgo);
}
// 日期范围筛选
if (this.startDate) {
const start = new Date(this.startDate).getTime();
result = result.filter(t => t.createTime >= start);
}
if (this.endDate) {
const end = new Date(this.endDate).getTime() + 24 * 60 * 60 * 1000;
result = result.filter(t => t.createTime < end);
}
return result;
},
stats() {
const filtered = this.filteredTickets;
const total = filtered.length;
const pending = filtered.filter(t => t.status === 'pending' || t.status === 'processing').length;
const resolved = filtered.filter(t => t.status === 'resolved' || t.status === 'closed').length;
const resolvedRate = total > 0 ? Math.round((resolved / total) * 100) : 0;
// 计算平均响应时间
const responseTimes = filtered
.filter(t => t.responseTime)
.map(t => (t.responseTime - t.createTime) / 60000);
const avgResponseTime = responseTimes.length > 0
? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
: 0;
// 计算平均解决时间
const resolveTimes = filtered
.filter(t => t.resolveTime)
.map(t => (t.resolveTime - t.createTime) / 60000);
const avgResolveTime = resolveTimes.length > 0
? Math.round(resolveTimes.reduce((a, b) => a + b, 0) / resolveTimes.length)
: 0;
// SLA超时数
const timeoutCount = filtered.filter(t =>
t.slaTimeRemaining <= 0 && t.status !== 'closed'
).length;
return {
total,
pending,
resolved,
resolvedRate,
avgResponseTime,
avgResolveTime,
timeoutCount
};
},
customerStats() {
const customerMap = {};
this.filteredTickets.forEach(ticket => {
if (!customerMap[ticket.customer]) {
customerMap[ticket.customer] = {
customer: ticket.customer,
count: 0,
responseTimes: [],
resolveTimes: [],
timeout: 0
};
}
customerMap[ticket.customer].count++;
if (ticket.responseTime) {
customerMap[ticket.customer].responseTimes.push(
(ticket.responseTime - ticket.createTime) / 60000
);
}
if (ticket.resolveTime) {
customerMap[ticket.customer].resolveTimes.push(
(ticket.resolveTime - ticket.createTime) / 60000
);
}
if (ticket.slaTimeRemaining <= 0 && ticket.status !== 'closed') {
customerMap[ticket.customer].timeout++;
}
});
return Object.values(customerMap).map(item => ({
customer: item.customer,
count: item.count,
avgResponse: item.responseTimes.length > 0
? Math.round(item.responseTimes.reduce((a, b) => a + b, 0) / item.responseTimes.length)
: 0,
avgResolve: item.resolveTimes.length > 0
? Math.round(item.resolveTimes.reduce((a, b) => a + b, 0) / item.resolveTimes.length)
: 0,
timeout: item.timeout
})).sort((a, b) => b.count - a.count);
}
},
mounted() {
this.loadTickets();
this.$nextTick(() => {
this.initCharts();
});
},
watch: {
filteredTickets: {
handler() {
this.$nextTick(() => {
this.updateCharts();
});
},
deep: true
}
},
methods: {
loadTickets() {
const saved = localStorage.getItem('downtimeTickets');
if (saved) {
this.tickets = JSON.parse(saved);
}
},
updateStats() {
this.$nextTick(() => {
this.updateCharts();
});
},
initCharts() {
// 状态分布饼图
const statusCtx = document.getElementById('statusChart');
if (statusCtx) {
this.charts.status = new Chart(statusCtx, {
type: 'pie',
data: {
labels: ['待响应', '处理中', '已解决', '已关闭'],
datasets: [{
label: '工单状态',
data: [1048, 735, 580, 484],
backgroundColor: [
'#f39c12',
'#3498db',
'#27ae60',
'#95a5a6'
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '工单状态分布',
font: {
size: 16
}
},
legend: {
position: 'bottom'
},
tooltip: {
enabled: true,
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
}
// 趋势折线图
const trendCtx = document.getElementById('trendChart');
if (trendCtx) {
this.charts.trend = new Chart(trendCtx, {
type: 'line',
data: {
labels: ['2026-1-5', '2026-1-6', '2026-1-7', '2026-1-8', '2026-1-9'],
datasets: [{
label: '工单数量',
data: [100,399,654,32,75],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 响应时间柱状图
const responseCtx = document.getElementById('responseTimeChart');
if (responseCtx) {
this.charts.responseTime = new Chart(responseCtx, {
type: 'bar',
data: {
labels: ['张三', '李四', '王五', '赵六', '钱七'],
datasets: [{
label: '平均响应时间(分钟)',
data: [15, 22, 18, 25, 12],
backgroundColor: [
'#3498db',
'#2980b9',
'#5dade2',
'#34495e',
'#2c3e50'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '响应时间(分钟)'
}
},
x: {
title: {
display: true,
text: '责任人'
}
}
},
plugins: {
legend: {
position: 'top'
},
title: {
display: true,
text: '各责任人平均响应时间统计'
}
}
}
});
}
// 责任人统计
const assigneeCtx = document.getElementById('assigneeChart');
if (assigneeCtx) {
this.charts.assignee = new Chart(assigneeCtx, {
type: 'bar',
data: {
labels: ['张三', '李四', '王五', '赵六', '钱七', '孙八'],
datasets: [{
label: '处理工单数',
data: [45, 38, 32, 28, 22, 18],
backgroundColor: [
'#27ae60',
'#2ecc71',
'#3498db',
'#9b59b6',
'#f1c40f',
'#e67e22'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '工单数量'
}
},
x: {
title: {
display: true,
text: '责任人'
}
}
},
plugins: {
legend: {
position: 'top'
},
title: {
display: true,
text: '责任人处理工单数量统计'
}
}
}
});
}
this.updateCharts();
},
updateCharts() {
const filtered = this.filteredTickets;
// 更新状态分布
if (this.charts.status) {
this.charts.status.data.datasets[0].data = [
filtered.filter(t => t.status === 'pending').length,
filtered.filter(t => t.status === 'processing').length,
filtered.filter(t => t.status === 'resolved').length,
filtered.filter(t => t.status === 'closed').length
];
this.charts.status.update();
}
// 更新趋势图(按日期统计)
if (this.charts.trend) {
const dateMap = {};
filtered.forEach(ticket => {
const date = new Date(ticket.createTime).toLocaleDateString('zh-CN');
dateMap[date] = (dateMap[date] || 0) + 1;
});
const dates = Object.keys(dateMap).sort();
this.charts.trend.data.labels = dates;
this.charts.trend.data.datasets[0].data = dates.map(d => dateMap[d]);
this.charts.trend.update();
}
// 更新响应时间(按责任人)
if (this.charts.responseTime) {
const assigneeMap = {};
filtered.forEach(ticket => {
if (ticket.responseTime) {
if (!assigneeMap[ticket.assignee]) {
assigneeMap[ticket.assignee] = [];
}
assigneeMap[ticket.assignee].push(
(ticket.responseTime - ticket.createTime) / 60000
);
}
});
const assignees = Object.keys(assigneeMap);
this.charts.responseTime.data.labels = assignees;
this.charts.responseTime.data.datasets[0].data = assignees.map(a => {
const times = assigneeMap[a];
return Math.round(times.reduce((x, y) => x + y, 0) / times.length);
});
this.charts.responseTime.update();
}
// 更新责任人统计
if (this.charts.assignee) {
const assigneeCount = {};
filtered.forEach(ticket => {
assigneeCount[ticket.assignee] = (assigneeCount[ticket.assignee] || 0) + 1;
});
const assignees = Object.keys(assigneeCount).sort((a, b) =>
assigneeCount[b] - assigneeCount[a]
).slice(0, 10);
this.charts.assignee.data.labels = assignees;
this.charts.assignee.data.datasets[0].data = assignees.map(a => assigneeCount[a]);
this.charts.assignee.update();
}
},
getStatusText(status) {
const statusMap = {
'pending': '待响应',
'processing': '处理中',
'resolved': '已解决',
'closed': '已关闭'
};
return statusMap[status] || status;
},
getStatusBadgeClass(status) {
const classMap = {
'pending': 'badge-warning',
'processing': 'badge-info',
'resolved': 'badge-success',
'closed': 'badge-secondary'
};
return classMap[status] || '';
},
formatTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return date.toLocaleString('zh-CN');
}
}
}).mount('#app');
</script>
</body>
</html>
# 停机工单 SLA 状态规则说明
++ /dev/null
# 停机工单 SLA 状态规则说明
## 一、SLA 基本规则
### 1. SLA 时间限制
- **标准时限**:2小时(120分钟)
- **计时单位**:毫秒(ms)
- **计算公式**`SLA剩余时间 = SLA时限 - (当前时间 - 工单创建时间)`
### 2. SLA 计时起点
- **开始时间**:工单创建时立即开始计时
- **记录字段**`slaStartTime`(工单创建时间戳)
- **初始值**`slaTimeRemaining = 2小时(7200000毫秒)`
### 3. SLA 计时停止条件
- 工单状态变为 `closed`(已关闭)时停止计时
- 已关闭的工单不再更新 SLA 剩余时间
---
## 二、SLA 状态分类
### 状态1:正常(Normal)
- **条件**`剩余时间 > 1小时(3600000毫秒)`
- **显示样式**:黄色背景,正常显示
- **说明**:工单在正常处理时限内
### 状态2:警告(Warning)
- **条件**`0 < 剩余时间 ≤ 1小时(3600000毫秒)`
- **显示样式**:橙色/红色背景,警告提示
- **说明**:工单即将超时,需要紧急处理
### 状态3:超时(Timeout)
- **条件**`剩余时间 ≤ 0`
- **显示样式**:红色背景,闪烁动画
- **说明**:工单已超过 SLA 时限,需要立即处理
---
## 三、SLA 状态判断逻辑
### 代码实现逻辑
```javascript
// SLA剩余时间计算
if (工单状态 !== 'closed' && slaStartTime存在) {
已用时间 = 当前时间 - slaStartTime
剩余时间 = Math.max(0, SLA时限 - 已用时间)
}
// 状态判断
if (剩余时间 <= 0) {
状态 = '超时'
} else if (剩余时间 < 1小时) {
状态 = '警告'
} else {
状态 = '正常'
}
```
### 状态更新频率
- **更新间隔**:每1秒更新一次
- **更新范围**:所有未关闭的工单
- **实时性**:页面实时显示倒计时
---
## 四、SLA 统计规则
### 1. 超时工单统计
- **统计条件**`slaTimeRemaining <= 0 && status !== 'closed'`
- **统计位置**:列表页统计卡片、统计&分析页
- **用途**:监控 SLA 达成情况
### 2. 平均响应时间
- **计算公式**`(响应时间 - 创建时间) / 工单数量`
- **单位**:分钟
- **统计范围**:所有已响应的工单
### 3. 平均解决时间
- **计算公式**`(解决时间 - 创建时间) / 工单数量`
- **单位**:分钟
- **统计范围**:所有已解决的工单
---
## 五、SLA 状态显示规则
### 1. 列表页显示
- **位置**:每个工单卡片下方
- **格式**`⏱️ SLA计时: X小时 X分钟 X秒`
- **颜色**:根据状态自动变色
### 2. 详情页显示
- **位置**:工单基本信息区域
- **格式**:大号显示,更醒目
- **动画**:超时时闪烁提醒
### 3. 统计页显示
- **位置**:核心指标统计卡片
- **内容**:SLA超时数量统计
- **图表**:响应时间分析图表
---
## 六、特殊情况处理
### 1. 工单关闭后
- **SLA计时停止**:不再更新剩余时间
- **状态保持**:保留关闭时的 SLA 状态
- **统计计入**:计入历史统计数据
### 2. 页面刷新
- **数据恢复**:从 localStorage 恢复工单数据
- **时间重算**:根据 `slaStartTime` 重新计算剩余时间
- **状态更新**:自动更新到当前状态
### 3. 跨天/跨月
- **时间计算**:基于时间戳,不受日期影响
- **精确计算**:毫秒级精度,确保准确性
---
## 七、SLA 规则配置
### 当前配置
```javascript
slaLimit: 2 * 60 * 60 * 1000 // 2小时 = 7200000毫秒
```
### 可调整参数
- **SLA时限**:可根据业务需求调整(建议范围:1-4小时)
- **警告阈值**:当前为1小时,可调整
- **更新频率**:当前为1秒,可调整
---
## 八、SLA 状态流转图
```
工单创建
SLA开始计时(2小时倒计时)
[正常状态] 剩余时间 > 1小时
[警告状态] 剩余时间 ≤ 1小时
[超时状态] 剩余时间 ≤ 0
工单关闭 → SLA计时停止
```
---
## 九、注意事项
1. **时间精度**:使用毫秒级时间戳,确保计算准确
2. **时区问题**:使用本地时间,确保显示正确
3. **性能优化**:定时器每秒更新,注意页面性能
4. **数据持久化**:SLA状态随工单数据一起保存
5. **状态一致性**:确保所有页面 SLA 状态显示一致
---
## 十、未来优化建议
1. **分级SLA**:根据客户等级设置不同的SLA时限
2. **暂停机制**:支持暂停SLA计时(如等待客户反馈)
3. **预警通知**:SLA即将超时前自动发送通知
4. **历史记录**:记录SLA状态变化历史
5. **报表导出**:SLA达成率报表导出功能
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务价格标准首页 - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.card {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
color: white;
padding: 25px;
border-radius: 8px;
text-align: center;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.stat-card.success {
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #e6a23c 0%, #ebb563 100%);
}
.stat-card.info {
background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%);
}
.stat-value {
font-size: 36px;
font-weight: bold;
margin: 15px 0;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
.whitebook-info {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 2px solid #409eff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.whitebook-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.whitebook-title {
font-size: 20px;
font-weight: 600;
color: #333;
}
.whitebook-version {
font-size: 18px;
color: #409eff;
font-weight: bold;
}
.whitebook-status {
display: inline-block;
padding: 4px 12px;
background: #67c23a;
color: white;
border-radius: 4px;
font-size: 12px;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.action-btn {
padding: 20px;
background: white;
border: 2px solid #e4e7ed;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
color: #333;
display: block;
}
.action-btn:hover {
border-color: #409eff;
background: #f0f9ff;
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.action-icon {
font-size: 32px;
margin-bottom: 10px;
}
.action-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 5px;
}
.action-desc {
font-size: 12px;
color: #909399;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<h1>服务价格标准化管理系统</h1>
</div>
<!-- 白皮书信息 -->
<div class="whitebook-info">
<div class="whitebook-header">
<div>
<div class="whitebook-title">当前生效的服务价格白皮书</div>
<div style="margin-top: 10px; color: #666; font-size: 14px;">版本号:<span class="whitebook-version">{{ whitebook.version }}</span></div>
</div>
<span class="whitebook-status">{{ whitebook.status }}</span>
</div>
<div style="color: #666; font-size: 14px;">生效时间:{{ whitebook.effectiveDate }}</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">人工费用标准数量</div>
<div class="stat-value">{{ stats.laborCount }}</div>
<div style="font-size: 12px; margin-top: 10px; opacity: 0.8;">已配置标准</div>
</div>
<div class="stat-card success">
<div class="stat-label">零件费用标准数量</div>
<div class="stat-value">{{ stats.partCount }}</div>
<div style="font-size: 12px; margin-top: 10px; opacity: 0.8;">已配置标准</div>
</div>
<div class="stat-card warning">
<div class="stat-label">已启用的报价规则</div>
<div class="stat-value">{{ stats.ruleCount }}</div>
<div style="font-size: 12px; margin-top: 10px; opacity: 0.8;">启用中</div>
</div>
</div>
<!-- 快捷入口 -->
<div class="card">
<div class="card-title">快捷入口</div>
<div class="quick-actions">
<a href="2-人工费用标准管理列表页.html" class="action-btn">
<div class="action-icon">👷</div>
<div class="action-title">人工费用管理</div>
<div class="action-desc">管理服务人工费用标准</div>
</a>
<a href="4-零件费用标准管理列表页.html" class="action-btn">
<div class="action-icon">🔧</div>
<div class="action-title">零件费用管理</div>
<div class="action-desc">管理零件费用标准</div>
</a>
<a href="6-报价规则配置列表页.html" class="action-btn">
<div class="action-icon">⚙️</div>
<div class="action-title">报价规则配置</div>
<div class="action-desc">配置报价规则</div>
</a>
<a href="10-服务价格白皮书查看页.html" class="action-btn">
<div class="action-icon">📄</div>
<div class="action-title">查看服务价格白皮书</div>
<div class="action-desc">查看完整价格标准文档</div>
</a>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
whitebook: {
version: 'V1.0',
status: '生效中',
effectiveDate: '2024-01-01'
},
stats: {
laborCount: 12,
partCount: 156,
ruleCount: 8
}
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务价格白皮书 - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.readonly-badge {
display: inline-block;
padding: 4px 12px;
background: #fef0f0;
color: #f56c6c;
border: 1px solid #f56c6c;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin-left: 15px;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 25px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.whitebook-header {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 2px solid #409eff;
border-radius: 8px;
padding: 25px;
margin-bottom: 30px;
text-align: center;
}
.whitebook-title {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
}
.whitebook-version {
font-size: 20px;
color: #409eff;
font-weight: 600;
margin-bottom: 10px;
}
.whitebook-meta {
font-size: 14px;
color: #606266;
margin-top: 10px;
}
.section {
margin-bottom: 40px;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
}
.section-content {
line-height: 1.8;
color: #606266;
font-size: 14px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border: 1px solid #e4e7ed;
}
.data-table th {
background: #f5f7fa;
font-weight: 600;
color: #333;
}
.data-table tr:nth-child(even) {
background: #fafafa;
}
.info-box {
background: #f5f7fa;
border-left: 4px solid #409eff;
padding: 15px 20px;
margin: 20px 0;
border-radius: 4px;
}
.info-box-title {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-default {
background: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.btn-default:hover {
background: #ecf5ff;
color: #409eff;
border-color: #409eff;
}
.highlight {
color: #409eff;
font-weight: 600;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<div>
<h1>服务价格白皮书 <span class="readonly-badge">只读模式</span></h1>
</div>
<button class="btn btn-default" @click="handleBack">返回首页</button>
</div>
</div>
<div class="card">
<!-- 白皮书头部 -->
<div class="whitebook-header">
<div class="whitebook-title">服务价格白皮书</div>
<div class="whitebook-version">版本号:{{ whitebook.version }}</div>
<div class="whitebook-meta">
<div>生效时间:{{ whitebook.effectiveDate }}</div>
<div>状态:<span class="highlight">{{ whitebook.status }}</span></div>
</div>
</div>
<!-- 适用范围说明 -->
<div class="section">
<div class="section-title">一、适用范围说明</div>
<div class="section-content">
<p>本服务价格白皮书适用于质保期外的所有服务项目,包括但不限于:</p>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>设备维修服务</li>
<li>技术支持服务</li>
<li>上门服务</li>
<li>远程服务</li>
<li>紧急服务</li>
</ul>
<div class="info-box">
<div class="info-box-title">重要提示</div>
<div>本白皮书中的所有价格标准均为质保期外服务的标准价格,质保期内服务按照合同约定执行。</div>
</div>
</div>
</div>
<!-- 人工费用标准 -->
<div class="section">
<div class="section-title">二、人工费用标准</div>
<div class="section-content">
<p>人工费用根据服务类型、技术等级和计费方式的不同,执行以下标准:</p>
<table class="data-table">
<thead>
<tr>
<th>服务类型</th>
<th>技术等级</th>
<th>计费方式</th>
<th>单价</th>
<th>最低计费时长</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in laborStandards" :key="index">
<td>{{ item.serviceType }}</td>
<td>{{ item.techLevel }}</td>
<td>{{ item.billingMethod }}</td>
<td>¥{{ item.price }}/{{ item.billingMethod === '按小时' ? '小时' : '次' }}</td>
<td>{{ item.minHours || '-' }}</td>
<td>{{ item.description }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 零件费用说明 -->
<div class="section">
<div class="section-title">三、零件费用说明</div>
<div class="section-content">
<p>零件费用按照标准零件价格表执行,主要包括:</p>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>零件价格以系统标准价格表为准</li>
<li>零件价格可能因市场波动而调整,以实际报价时价格为准</li>
<li>部分零件价格含税,部分不含税,具体以报价单为准</li>
<li>零件数量以实际使用为准</li>
</ul>
<div class="info-box" style="margin-top: 20px;">
<div class="info-box-title">零件价格查询</div>
<div>详细的零件价格标准请参考系统中的零件费用标准管理模块,或联系客服获取最新价格信息。</div>
</div>
</div>
</div>
<!-- 特殊/加急服务价格说明 -->
<div class="section">
<div class="section-title">四、特殊/加急服务价格说明</div>
<div class="section-content">
<p>对于紧急服务或特殊时间段的服务,可能产生额外的加急费用:</p>
<table class="data-table">
<thead>
<tr>
<th>服务类型</th>
<th>加急费类型</th>
<th>加急费标准</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>紧急服务</td>
<td>百分比</td>
<td>加收20%</td>
<td>在标准人工费用基础上加收20%</td>
</tr>
<tr>
<td>非工作时间服务</td>
<td>百分比</td>
<td>加收30%</td>
<td>节假日、夜间等非工作时间提供服务</td>
</tr>
<tr>
<td>超远距离服务</td>
<td>固定金额</td>
<td>加收500元</td>
<td>距离超过50公里的上门服务</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 报价说明 -->
<div class="section">
<div class="section-title">五、报价说明</div>
<div class="section-content">
<p>标准报价单的生成遵循以下规则:</p>
<ol style="margin-left: 20px; margin-top: 10px;">
<li><strong>自动匹配规则:</strong>系统根据服务场景(停机/非停机)自动匹配相应的报价规则</li>
<li><strong>人工费用计算:</strong>根据选择的服务类型和技术等级,按照标准价格计算</li>
<li><strong>零件费用计算:</strong>根据实际使用的零件数量和标准价格计算</li>
<li><strong>加急费用:</strong>如适用,按照加急费规则计算并累加</li>
<li><strong>折扣规则:</strong>如适用,按照折扣规则进行减免</li>
<li><strong>最终价格:</strong>所有费用项目相加得出最终报价</li>
</ol>
<div class="info-box" style="margin-top: 20px;">
<div class="info-box-title">报价有效期</div>
<div>标准报价单的有效期为30天,超过有效期需重新生成报价单。最终价格以实际服务完成后的结算为准。</div>
</div>
</div>
</div>
<!-- 数据来源说明 -->
<div class="section">
<div class="section-title">六、数据来源说明</div>
<div class="section-content">
<p>本白皮书中的所有价格数据均来源于系统价格配置模块:</p>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>人工费用标准:来源于"人工费用标准管理"模块</li>
<li>零件费用标准:来源于"零件费用标准管理"模块</li>
<li>报价规则:来源于"报价规则配置"模块</li>
</ul>
<div class="info-box" style="margin-top: 20px;">
<div class="info-box-title">重要提示</div>
<div>本白皮书为只读文档,所有价格配置的修改需在相应的管理模块中进行。价格调整后,系统将自动更新白皮书版本。</div>
</div>
</div>
</div>
<!-- 联系方式 -->
<div class="section">
<div class="section-title">七、联系方式</div>
<div class="section-content">
<p>如有任何疑问或需要了解更多信息,请联系:</p>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>客服热线:400-XXX-XXXX</li>
<li>技术支持邮箱:support@example.com</li>
<li>工作时间:周一至周五 9:00-18:00</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
whitebook: {
version: 'V1.0',
effectiveDate: '2024-01-01',
status: '生效中'
},
laborStandards: [
{
serviceType: '上门',
techLevel: '普通',
billingMethod: '按小时',
price: 150,
minHours: 2,
description: '标准上门服务,最低计费2小时'
},
{
serviceType: '上门',
techLevel: '高级',
billingMethod: '按小时',
price: 250,
minHours: 2,
description: '高级技术上门服务,最低计费2小时'
},
{
serviceType: '远程',
techLevel: '普通',
billingMethod: '按次',
price: 200,
minHours: null,
description: '远程技术支持服务'
},
{
serviceType: '远程',
techLevel: '高级',
billingMethod: '按次',
price: 300,
minHours: null,
description: '高级远程技术支持服务'
},
{
serviceType: '紧急',
techLevel: '普通',
billingMethod: '按小时',
price: 300,
minHours: 1,
description: '紧急上门服务,最低计费1小时'
},
{
serviceType: '紧急',
techLevel: '高级',
billingMethod: '按小时',
price: 400,
minHours: 1,
description: '高级紧急上门服务,最低计费1小时'
}
]
}
},
methods: {
handleBack() {
window.location.href = '1-服务价格标准首页.html';
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人工费用标准管理 - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.nav-menu {
display: flex;
gap: 10px;
}
.nav-item {
padding: 8px 16px;
background: #409eff;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
background: #66b1ff;
}
.nav-item.active {
background: #67c23a;
}
.card {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-success {
background: #67c23a;
color: white;
}
.btn-success:hover {
background: #85ce61;
}
.btn-danger {
background: #f56c6c;
color: white;
}
.btn-danger:hover {
background: #f78989;
}
.btn-warning {
background: #e6a23c;
color: white;
}
.btn-warning:hover {
background: #ebb563;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e4e7ed;
}
.data-table th {
background: #f5f7fa;
font-weight: 600;
color: #333;
}
.data-table tr:hover {
background: #f5f7fa;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: #f0f9ff;
color: #67c23a;
border: 1px solid #67c23a;
}
.badge-danger {
background: #fef0f0;
color: #f56c6c;
border: 1px solid #f56c6c;
}
.action-btns {
display: flex;
gap: 8px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>人工费用标准管理</h1>
<div class="nav-menu">
<a href="1-服务价格标准首页.html" class="nav-item">首页</a>
<a href="2-人工费用标准管理列表页.html" class="nav-item active">人工费用</a>
<a href="4-零件费用标准管理列表页.html" class="nav-item">零件费用</a>
<a href="6-报价规则配置列表页.html" class="nav-item">报价规则</a>
</div>
</div>
</div>
<div class="card">
<div class="toolbar">
<div class="card-title">人工费用标准列表</div>
<button class="btn btn-primary" @click="handleAdd">+ 新增人工费用</button>
</div>
<table class="data-table">
<thead>
<tr>
<th>服务类型</th>
<th>技术等级</th>
<th>计费方式</th>
<th>单价</th>
<th>最低计费时长</th>
<th>生效时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in laborList" :key="item.id">
<td>{{ item.serviceType }}</td>
<td>{{ item.techLevel }}</td>
<td>{{ item.billingMethod }}</td>
<td>¥{{ item.price }}/{{ item.billingMethod === '按小时' ? '小时' : '次' }}</td>
<td>{{ item.minHours || '-' }}</td>
<td>{{ item.effectiveDate }}</td>
<td>
<span :class="item.status === '启用' ? 'badge badge-success' : 'badge badge-danger'">
{{ item.status }}
</span>
</td>
<td>
<div class="action-btns">
<button class="btn btn-primary btn-small" @click="handleEdit(item)">编辑</button>
<button
v-if="item.status === '启用'"
class="btn btn-warning btn-small"
@click="handleToggleStatus(item)">
停用
</button>
<button
v-else
class="btn btn-success btn-small"
@click="handleToggleStatus(item)">
启用
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
laborList: [
{
id: 1,
serviceType: '上门',
techLevel: '普通',
billingMethod: '按小时',
price: 150,
minHours: 2,
effectiveDate: '2024-01-01',
status: '启用'
},
{
id: 2,
serviceType: '上门',
techLevel: '高级',
billingMethod: '按小时',
price: 250,
minHours: 2,
effectiveDate: '2024-01-01',
status: '启用'
},
{
id: 3,
serviceType: '远程',
techLevel: '普通',
billingMethod: '按次',
price: 200,
minHours: null,
effectiveDate: '2024-01-01',
status: '启用'
},
{
id: 4,
serviceType: '紧急',
techLevel: '高级',
billingMethod: '按小时',
price: 400,
minHours: 1,
effectiveDate: '2024-01-01',
status: '停用'
}
]
}
},
methods: {
handleAdd() {
window.location.href = '3-人工费用标准新增编辑页.html?action=add';
},
handleEdit(item) {
window.location.href = `3-人工费用标准新增编辑页.html?action=edit&id=${item.id}`;
},
handleToggleStatus(item) {
item.status = item.status === '启用' ? '停用' : '启用';
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人工费用标准{{ isEdit ? '编辑' : '新增' }} - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 25px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.form-label .required {
color: #f56c6c;
margin-left: 4px;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #409eff;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
.btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-default {
background: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.btn-default:hover {
background: #ecf5ff;
color: #409eff;
border-color: #409eff;
}
.form-hint {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<h1>人工费用标准{{ isEdit ? '编辑' : '新增' }}</h1>
</div>
<div class="card">
<div class="card-title">基本信息</div>
<form @submit.prevent="handleSubmit">
<div class="form-row">
<div class="form-group">
<label class="form-label">服务类型 <span class="required">*</span></label>
<select v-model="form.serviceType" class="form-control" required>
<option value="">请选择</option>
<option value="上门">上门</option>
<option value="远程">远程</option>
<option value="紧急">紧急</option>
</select>
</div>
<div class="form-group">
<label class="form-label">技术等级 <span class="required">*</span></label>
<select v-model="form.techLevel" class="form-control" required>
<option value="">请选择</option>
<option value="普通">普通</option>
<option value="高级">高级</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">计费方式 <span class="required">*</span></label>
<select v-model="form.billingMethod" class="form-control" required>
<option value="">请选择</option>
<option value="按小时">按小时</option>
<option value="按次">按次</option>
</select>
</div>
<div class="form-group">
<label class="form-label">单价 <span class="required">*</span></label>
<input
type="number"
v-model.number="form.price"
class="form-control"
placeholder="请输入单价"
step="0.01"
min="0"
required
/>
<div class="form-hint">单位:元/{{ form.billingMethod || '次或小时' }}</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">最低计费时长</label>
<input
type="number"
v-model.number="form.minHours"
class="form-control"
placeholder="请输入最低计费时长"
min="0"
step="0.5"
/>
<div class="form-hint">仅按小时计费时有效,单位:小时</div>
</div>
<div class="form-group">
<label class="form-label">生效时间 <span class="required">*</span></label>
<input
type="date"
v-model="form.effectiveDate"
class="form-control"
required
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">失效时间</label>
<input
type="date"
v-model="form.expiryDate"
class="form-control"
/>
<div class="form-hint">留空表示长期有效</div>
</div>
</div>
<div class="form-group">
<label class="form-label">备注</label>
<textarea
v-model="form.remark"
class="form-control"
rows="4"
placeholder="请输入备注信息"
></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-default" @click="handleCancel">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
const urlParams = new URLSearchParams(window.location.search);
const action = urlParams.get('action');
const id = urlParams.get('id');
// 模拟编辑数据
const editData = {
1: {
serviceType: '上门',
techLevel: '普通',
billingMethod: '按小时',
price: 150,
minHours: 2,
effectiveDate: '2024-01-01',
expiryDate: '',
remark: '标准上门服务费用'
}
};
return {
isEdit: action === 'edit',
form: {
serviceType: '',
techLevel: '',
billingMethod: '',
price: null,
minHours: null,
effectiveDate: '',
expiryDate: '',
remark: ''
}
}
},
mounted() {
if (this.isEdit) {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
// 模拟加载数据
if (id === '1') {
this.form = {
serviceType: '上门',
techLevel: '普通',
billingMethod: '按小时',
price: 150,
minHours: 2,
effectiveDate: '2024-01-01',
expiryDate: '',
remark: '标准上门服务费用'
};
}
}
},
methods: {
handleSubmit() {
alert(this.isEdit ? '保存成功!' : '新增成功!');
window.location.href = '2-人工费用标准管理列表页.html';
},
handleCancel() {
if (confirm('确定要取消吗?未保存的数据将丢失。')) {
window.location.href = '2-人工费用标准管理列表页.html';
}
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>零件费用标准管理 - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.nav-menu {
display: flex;
gap: 10px;
}
.nav-item {
padding: 8px 16px;
background: #409eff;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
background: #66b1ff;
}
.nav-item.active {
background: #67c23a;
}
.card {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-success {
background: #67c23a;
color: white;
}
.btn-success:hover {
background: #85ce61;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e4e7ed;
}
.data-table th {
background: #f5f7fa;
font-weight: 600;
color: #333;
}
.data-table tr:hover {
background: #f5f7fa;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: #f0f9ff;
color: #67c23a;
border: 1px solid #67c23a;
}
.badge-danger {
background: #fef0f0;
color: #f56c6c;
border: 1px solid #f56c6c;
}
.action-btns {
display: flex;
gap: 8px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>零件费用标准管理</h1>
<div class="nav-menu">
<a href="1-服务价格标准首页.html" class="nav-item">首页</a>
<a href="2-人工费用标准管理列表页.html" class="nav-item">人工费用</a>
<a href="4-零件费用标准管理列表页.html" class="nav-item active">零件费用</a>
<a href="6-报价规则配置列表页.html" class="nav-item">报价规则</a>
</div>
</div>
</div>
<div class="card">
<div class="toolbar">
<div class="card-title">零件费用标准列表</div>
<div style="display: flex; gap: 10px;">
<button class="btn btn-success" @click="handleImport">批量导入</button>
<button class="btn btn-primary" @click="handleAdd">+ 新增零件</button>
</div>
</div>
<table class="data-table">
<thead>
<tr>
<th>零件编号</th>
<th>零件名称</th>
<th>型号规格</th>
<th>标准单价</th>
<th>单位</th>
<th>适用设备</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in partList" :key="item.id">
<td>{{ item.partNo }}</td>
<td>{{ item.partName }}</td>
<td>{{ item.specification }}</td>
<td>¥{{ item.price }}</td>
<td>{{ item.unit }}</td>
<td>{{ item.applicableEquipment }}</td>
<td>
<span :class="item.status === '启用' ? 'badge badge-success' : 'badge badge-danger'">
{{ item.status }}
</span>
</td>
<td>
<div class="action-btns">
<button class="btn btn-primary btn-small" @click="handleEdit(item)">编辑</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
partList: [
{
id: 1,
partNo: 'P001',
partName: '服务器硬盘',
specification: '2TB SATA',
price: 800,
unit: '块',
applicableEquipment: '服务器',
status: '启用'
},
{
id: 2,
partNo: 'P002',
partName: '内存条',
specification: '16GB DDR4',
price: 600,
unit: '条',
applicableEquipment: '服务器/工作站',
status: '启用'
},
{
id: 3,
partNo: 'P003',
partName: '网络交换机',
specification: '24口千兆',
price: 2000,
unit: '台',
applicableEquipment: '网络设备',
status: '启用'
},
{
id: 4,
partNo: 'P004',
partName: 'UPS电源',
specification: '3KVA',
price: 3500,
unit: '台',
applicableEquipment: '电源设备',
status: '启用'
}
]
}
},
methods: {
handleAdd() {
window.location.href = '5-零件费用标准新增编辑页.html?action=add';
},
handleEdit(item) {
window.location.href = `5-零件费用标准新增编辑页.html?action=edit&id=${item.id}`;
},
handleImport() {
alert('批量导入功能(占位)');
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>零件费用标准{{ isEdit ? '编辑' : '新增' }} - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 25px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.form-label .required {
color: #f56c6c;
margin-left: 4px;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #409eff;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
.btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-default {
background: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.btn-default:hover {
background: #ecf5ff;
color: #409eff;
border-color: #409eff;
}
.form-hint {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #409eff;
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<h1>零件费用标准{{ isEdit ? '编辑' : '新增' }}</h1>
</div>
<div class="card">
<div class="card-title">基本信息</div>
<form @submit.prevent="handleSubmit">
<div class="form-row">
<div class="form-group">
<label class="form-label">零件编号 <span class="required">*</span></label>
<input
type="text"
v-model="form.partNo"
class="form-control"
placeholder="请输入零件编号"
required
/>
</div>
<div class="form-group">
<label class="form-label">零件名称 <span class="required">*</span></label>
<input
type="text"
v-model="form.partName"
class="form-control"
placeholder="请输入零件名称"
required
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">型号规格 <span class="required">*</span></label>
<input
type="text"
v-model="form.specification"
class="form-control"
placeholder="请输入型号规格"
required
/>
</div>
<div class="form-group">
<label class="form-label">单位 <span class="required">*</span></label>
<input
type="text"
v-model="form.unit"
class="form-control"
placeholder="如:块、条、台等"
required
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">标准单价 <span class="required">*</span></label>
<input
type="number"
v-model.number="form.price"
class="form-control"
placeholder="请输入标准单价"
step="0.01"
min="0"
required
/>
<div class="form-hint">单位:元</div>
</div>
<div class="form-group">
<label class="form-label">是否含税</label>
<div style="margin-top: 8px;">
<label class="switch">
<input type="checkbox" v-model="form.taxIncluded">
<span class="slider"></span>
</label>
<span style="margin-left: 10px; color: #606266;">{{ form.taxIncluded ? '含税' : '不含税' }}</span>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">生效时间 <span class="required">*</span></label>
<input
type="date"
v-model="form.effectiveDate"
class="form-control"
required
/>
</div>
<div class="form-group">
<label class="form-label">备注</label>
<textarea
v-model="form.remark"
class="form-control"
rows="4"
placeholder="请输入备注信息"
></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-default" @click="handleCancel">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
const urlParams = new URLSearchParams(window.location.search);
const action = urlParams.get('action');
const id = urlParams.get('id');
return {
isEdit: action === 'edit',
form: {
partNo: '',
partName: '',
specification: '',
unit: '',
price: null,
taxIncluded: true,
effectiveDate: '',
remark: ''
}
}
},
mounted() {
if (this.isEdit) {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
// 模拟加载数据
if (id === '1') {
this.form = {
partNo: 'P001',
partName: '服务器硬盘',
specification: '2TB SATA',
unit: '块',
price: 800,
taxIncluded: true,
effectiveDate: '2024-01-01',
remark: '标准服务器硬盘'
};
}
}
},
methods: {
handleSubmit() {
alert(this.isEdit ? '保存成功!' : '新增成功!');
window.location.href = '4-零件费用标准管理列表页.html';
},
handleCancel() {
if (confirm('确定要取消吗?未保存的数据将丢失。')) {
window.location.href = '4-零件费用标准管理列表页.html';
}
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>报价规则配置 - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.nav-menu {
display: flex;
gap: 10px;
}
.nav-item {
padding: 8px 16px;
background: #409eff;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.nav-item:hover {
background: #66b1ff;
}
.nav-item.active {
background: #67c23a;
}
.card {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-success {
background: #67c23a;
color: white;
}
.btn-success:hover {
background: #85ce61;
}
.btn-warning {
background: #e6a23c;
color: white;
}
.btn-warning:hover {
background: #ebb563;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e4e7ed;
}
.data-table th {
background: #f5f7fa;
font-weight: 600;
color: #333;
}
.data-table tr:hover {
background: #f5f7fa;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: #f0f9ff;
color: #67c23a;
border: 1px solid #67c23a;
}
.badge-danger {
background: #fef0f0;
color: #f56c6c;
border: 1px solid #f56c6c;
}
.action-btns {
display: flex;
gap: 8px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>报价规则配置</h1>
<div class="nav-menu">
<a href="1-服务价格标准首页.html" class="nav-item">首页</a>
<a href="2-人工费用标准管理列表页.html" class="nav-item">人工费用</a>
<a href="4-零件费用标准管理列表页.html" class="nav-item">零件费用</a>
<a href="6-报价规则配置列表页.html" class="nav-item active">报价规则</a>
</div>
</div>
</div>
<div class="card">
<div class="toolbar">
<div class="card-title">报价规则列表</div>
<button class="btn btn-primary" @click="handleAdd">+ 新增规则</button>
</div>
<table class="data-table">
<thead>
<tr>
<th>规则名称</th>
<th>适用场景</th>
<th>是否质保期外</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in ruleList" :key="item.id">
<td>{{ item.ruleName }}</td>
<td>{{ item.scenario }}</td>
<td></td>
<td>
<span :class="item.status === '启用' ? 'badge badge-success' : 'badge badge-danger'">
{{ item.status }}
</span>
</td>
<td>
<div class="action-btns">
<button class="btn btn-primary btn-small" @click="handleEdit(item)">编辑</button>
<button
v-if="item.status === '启用'"
class="btn btn-warning btn-small"
@click="handleToggleStatus(item)">
停用
</button>
<button
v-else
class="btn btn-success btn-small"
@click="handleToggleStatus(item)">
启用
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
ruleList: [
{
id: 1,
ruleName: '停机服务标准报价规则',
scenario: '停机',
status: '启用'
},
{
id: 2,
ruleName: '非停机服务标准报价规则',
scenario: '非停机',
status: '启用'
},
{
id: 3,
ruleName: '紧急服务加急报价规则',
scenario: '停机',
status: '启用'
}
]
}
},
methods: {
handleAdd() {
window.location.href = '7-报价规则配置新增编辑页.html?action=add';
},
handleEdit(item) {
window.location.href = `7-报价规则配置新增编辑页.html?action=edit&id=${item.id}`;
},
handleToggleStatus(item) {
item.status = item.status === '启用' ? '停用' : '启用';
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>报价规则配置{{ isEdit ? '编辑' : '新增' }} - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 25px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.form-label .required {
color: #f56c6c;
margin-left: 4px;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #409eff;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
.btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-default {
background: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.btn-default:hover {
background: #ecf5ff;
color: #409eff;
border-color: #409eff;
}
.form-hint {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.rule-section {
background: #f5f7fa;
padding: 20px;
border-radius: 6px;
margin-bottom: 20px;
}
.rule-section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
}
.select-multiple {
min-height: 100px;
max-height: 200px;
overflow-y: auto;
border: 1px solid #dcdfe6;
border-radius: 6px;
padding: 10px;
}
.select-item {
padding: 8px;
margin-bottom: 5px;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.select-item:hover {
background: #ecf5ff;
}
.select-item.selected {
background: #409eff;
color: white;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<h1>报价规则配置{{ isEdit ? '编辑' : '新增' }}</h1>
</div>
<div class="card">
<div class="card-title">基本信息</div>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label class="form-label">规则名称 <span class="required">*</span></label>
<input
type="text"
v-model="form.ruleName"
class="form-control"
placeholder="请输入规则名称"
required
/>
</div>
<div class="form-group">
<label class="form-label">适用服务场景 <span class="required">*</span></label>
<select v-model="form.scenario" class="form-control" required>
<option value="">请选择</option>
<option value="停机">停机</option>
<option value="非停机">非停机</option>
</select>
</div>
<!-- 人工费用规则 -->
<div class="rule-section">
<div class="rule-section-title">人工费用规则 <span class="required">*</span></div>
<div class="select-multiple">
<div
v-for="labor in availableLabors"
:key="labor.id"
:class="['select-item', { selected: form.laborRules.includes(labor.id) }]"
@click="toggleLaborRule(labor.id)"
>
{{ labor.serviceType }} - {{ labor.techLevel }} - {{ labor.billingMethod }} (¥{{ labor.price }}/{{ labor.billingMethod === '按小时' ? '小时' : '次' }})
</div>
</div>
<div class="form-hint">点击选择适用的人工费用标准(可多选)</div>
</div>
<!-- 零件费用规则 -->
<div class="rule-section">
<div class="rule-section-title">零件费用规则</div>
<div class="select-multiple">
<div
v-for="part in availableParts"
:key="part.id"
:class="['select-item', { selected: form.partRules.includes(part.id) }]"
@click="togglePartRule(part.id)"
>
{{ part.partName }} ({{ part.specification }}) - ¥{{ part.price }}
</div>
</div>
<div class="form-hint">点击选择适用的零件费用标准(可多选,可选)</div>
</div>
<!-- 加急费规则 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">加急费规则类型</label>
<select v-model="form.urgentFeeType" class="form-control">
<option value="">无加急费</option>
<option value="percentage">百分比</option>
<option value="fixed">固定金额</option>
</select>
</div>
<div class="form-group" v-if="form.urgentFeeType">
<label class="form-label">加急费数值</label>
<input
type="number"
v-model.number="form.urgentFeeValue"
class="form-control"
placeholder="请输入数值"
step="0.01"
min="0"
/>
<div class="form-hint">{{ form.urgentFeeType === 'percentage' ? '单位:%(例如:20表示加收20%)' : '单位:元' }}</div>
</div>
</div>
<!-- 折扣规则 -->
<div class="form-row">
<div class="form-group">
<label class="form-label">折扣规则类型</label>
<select v-model="form.discountType" class="form-control">
<option value="">无折扣</option>
<option value="percentage">百分比折扣</option>
<option value="fixed">固定金额减免</option>
</select>
</div>
<div class="form-group" v-if="form.discountType">
<label class="form-label">折扣数值</label>
<input
type="number"
v-model.number="form.discountValue"
class="form-control"
placeholder="请输入数值"
step="0.01"
min="0"
/>
<div class="form-hint">{{ form.discountType === 'percentage' ? '单位:%(例如:10表示打9折)' : '单位:元' }}</div>
</div>
</div>
<div class="form-group">
<label class="form-label">规则说明</label>
<textarea
v-model="form.description"
class="form-control"
rows="4"
placeholder="请输入规则说明"
></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-default" @click="handleCancel">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
const urlParams = new URLSearchParams(window.location.search);
const action = urlParams.get('action');
return {
isEdit: action === 'edit',
form: {
ruleName: '',
scenario: '',
laborRules: [],
partRules: [],
urgentFeeType: '',
urgentFeeValue: null,
discountType: '',
discountValue: null,
description: ''
},
availableLabors: [
{ id: 1, serviceType: '上门', techLevel: '普通', billingMethod: '按小时', price: 150 },
{ id: 2, serviceType: '上门', techLevel: '高级', billingMethod: '按小时', price: 250 },
{ id: 3, serviceType: '远程', techLevel: '普通', billingMethod: '按次', price: 200 },
{ id: 4, serviceType: '紧急', techLevel: '高级', billingMethod: '按小时', price: 400 }
],
availableParts: [
{ id: 1, partName: '服务器硬盘', specification: '2TB SATA', price: 800 },
{ id: 2, partName: '内存条', specification: '16GB DDR4', price: 600 },
{ id: 3, partName: '网络交换机', specification: '24口千兆', price: 2000 }
]
}
},
mounted() {
if (this.isEdit) {
// 模拟加载数据
this.form = {
ruleName: '停机服务标准报价规则',
scenario: '停机',
laborRules: [1, 2],
partRules: [1, 2],
urgentFeeType: 'percentage',
urgentFeeValue: 20,
discountType: '',
discountValue: null,
description: '适用于停机场景的标准报价规则'
};
}
},
methods: {
toggleLaborRule(id) {
const index = this.form.laborRules.indexOf(id);
if (index > -1) {
this.form.laborRules.splice(index, 1);
} else {
this.form.laborRules.push(id);
}
},
togglePartRule(id) {
const index = this.form.partRules.indexOf(id);
if (index > -1) {
this.form.partRules.splice(index, 1);
} else {
this.form.partRules.push(id);
}
},
handleSubmit() {
if (this.form.laborRules.length === 0) {
alert('请至少选择一个人工费用规则');
return;
}
alert(this.isEdit ? '保存成功!' : '新增成功!');
window.location.href = '6-报价规则配置列表页.html';
},
handleCancel() {
if (confirm('确定要取消吗?未保存的数据将丢失。')) {
window.location.href = '6-报价规则配置列表页.html';
}
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>标准报价单生成 - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 25px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.form-label .required {
color: #f56c6c;
margin-left: 4px;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #409eff;
}
.form-control[readonly] {
background: #f5f7fa;
color: #606266;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
.btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-default {
background: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.btn-default:hover {
background: #ecf5ff;
color: #409eff;
border-color: #409eff;
}
.quote-detail {
background: #f5f7fa;
padding: 20px;
border-radius: 6px;
margin-top: 20px;
}
.quote-detail-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
}
.detail-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.detail-table th,
.detail-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #e4e7ed;
}
.detail-table th {
background: white;
font-weight: 600;
color: #333;
}
.total-row {
font-weight: 600;
font-size: 16px;
color: #409eff;
}
.info-box {
background: #ecf5ff;
border: 1px solid #409eff;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
}
.info-box-title {
font-weight: 600;
color: #409eff;
margin-bottom: 8px;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #409eff;
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<h1>标准报价单生成</h1>
</div>
<div class="card">
<div class="card-title">报价信息</div>
<form @submit.prevent="handleGenerate">
<div class="form-row">
<div class="form-group">
<label class="form-label">客户名称 <span class="required">*</span></label>
<input
type="text"
v-model="form.customerName"
class="form-control"
placeholder="请输入客户名称"
required
/>
</div>
<div class="form-group">
<label class="form-label">是否停机 <span class="required">*</span></label>
<select v-model="form.isDowntime" class="form-control" required @change="updateRule">
<option value="">请选择</option>
<option value="是"></option>
<option value="否"></option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">服务内容描述 <span class="required">*</span></label>
<textarea
v-model="form.serviceDescription"
class="form-control"
rows="4"
placeholder="请输入服务内容描述"
required
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">是否质保期外</label>
<div style="margin-top: 8px;">
<label class="switch">
<input type="checkbox" v-model="form.isOutOfWarranty" checked disabled>
<span class="slider"></span>
</label>
<span style="margin-left: 10px; color: #606266;">是(固定)</span>
</div>
</div>
</div>
<!-- 自动匹配的报价规则 -->
<div class="info-box" v-if="matchedRule">
<div class="info-box-title">匹配的报价规则</div>
<div>规则名称:{{ matchedRule.ruleName }}</div>
<div>适用场景:{{ matchedRule.scenario }}</div>
</div>
<!-- 人工费用明细 -->
<div class="quote-detail" v-if="matchedRule">
<div class="quote-detail-title">人工费用明细(自动计算)</div>
<table class="detail-table">
<thead>
<tr>
<th>服务类型</th>
<th>技术等级</th>
<th>计费方式</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in laborDetails" :key="index">
<td>{{ item.serviceType }}</td>
<td>{{ item.techLevel }}</td>
<td>{{ item.billingMethod }}</td>
<td>
<input
type="number"
v-model.number="item.quantity"
class="form-control"
style="width: 80px; display: inline-block;"
min="0"
step="0.5"
@input="calculateTotal"
/>
<span style="margin-left: 5px;">{{ item.billingMethod === '按小时' ? '小时' : '次' }}</span>
</td>
<td>¥{{ item.price }}/{{ item.billingMethod === '按小时' ? '小时' : '次' }}</td>
<td>¥{{ (item.price * item.quantity).toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 零件费用明细 -->
<div class="quote-detail">
<div class="quote-detail-title">零件费用明细(可选)</div>
<table class="detail-table">
<thead>
<tr>
<th>零件名称</th>
<th>型号规格</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in partDetails" :key="index">
<td>
<select v-model="item.partId" class="form-control" @change="updatePartInfo(index)">
<option value="">请选择零件</option>
<option v-for="part in availableParts" :key="part.id" :value="part.id">
{{ part.partName }}
</option>
</select>
</td>
<td>{{ item.specification }}</td>
<td>
<input
type="number"
v-model.number="item.quantity"
class="form-control"
style="width: 80px; display: inline-block;"
min="0"
@input="calculateTotal"
/>
</td>
<td>¥{{ item.price }}</td>
<td>¥{{ (item.price * item.quantity).toFixed(2) }}</td>
<td>
<button type="button" class="btn btn-danger btn-small" @click="removePart(index)">删除</button>
</td>
</tr>
<tr>
<td colspan="6">
<button type="button" class="btn btn-primary btn-small" @click="addPart">+ 添加零件</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 总计 -->
<div class="quote-detail" v-if="matchedRule">
<table class="detail-table">
<tbody>
<tr>
<td style="text-align: right; font-weight: 600;">人工费用小计:</td>
<td>¥{{ laborSubtotal.toFixed(2) }}</td>
</tr>
<tr>
<td style="text-align: right; font-weight: 600;">零件费用小计:</td>
<td>¥{{ partSubtotal.toFixed(2) }}</td>
</tr>
<tr class="total-row">
<td style="text-align: right;">总计:</td>
<td>¥{{ totalAmount.toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="form-actions">
<button type="button" class="btn btn-default" @click="handleCancel">取消</button>
<button type="submit" class="btn btn-primary" :disabled="!matchedRule">生成报价单</button>
</div>
</form>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
form: {
customerName: '',
serviceDescription: '',
isDowntime: '',
isOutOfWarranty: true
},
matchedRule: null,
laborDetails: [],
partDetails: [],
availableParts: [
{ id: 1, partName: '服务器硬盘', specification: '2TB SATA', price: 800 },
{ id: 2, partName: '内存条', specification: '16GB DDR4', price: 600 },
{ id: 3, partName: '网络交换机', specification: '24口千兆', price: 2000 }
],
availableRules: [
{
id: 1,
ruleName: '停机服务标准报价规则',
scenario: '停机',
laborRules: [
{ id: 1, serviceType: '上门', techLevel: '普通', billingMethod: '按小时', price: 150 },
{ id: 2, serviceType: '上门', techLevel: '高级', billingMethod: '按小时', price: 250 }
]
},
{
id: 2,
ruleName: '非停机服务标准报价规则',
scenario: '非停机',
laborRules: [
{ id: 3, serviceType: '远程', techLevel: '普通', billingMethod: '按次', price: 200 }
]
}
]
}
},
computed: {
laborSubtotal() {
return this.laborDetails.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
partSubtotal() {
return this.partDetails.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
totalAmount() {
return this.laborSubtotal + this.partSubtotal;
}
},
methods: {
updateRule() {
if (this.form.isDowntime) {
const scenario = this.form.isDowntime === '是' ? '停机' : '非停机';
this.matchedRule = this.availableRules.find(r => r.scenario === scenario);
if (this.matchedRule) {
this.laborDetails = this.matchedRule.laborRules.map(rule => ({
...rule,
quantity: 1
}));
}
} else {
this.matchedRule = null;
this.laborDetails = [];
}
this.calculateTotal();
},
updatePartInfo(index) {
const part = this.availableParts.find(p => p.id === this.partDetails[index].partId);
if (part) {
this.partDetails[index].partName = part.partName;
this.partDetails[index].specification = part.specification;
this.partDetails[index].price = part.price;
this.partDetails[index].quantity = 1;
}
this.calculateTotal();
},
addPart() {
this.partDetails.push({
partId: '',
partName: '',
specification: '',
price: 0,
quantity: 0
});
},
removePart(index) {
this.partDetails.splice(index, 1);
this.calculateTotal();
},
calculateTotal() {
// 计算逻辑已在computed中实现
},
handleGenerate() {
if (!this.matchedRule) {
alert('请先选择是否停机,系统将自动匹配报价规则');
return;
}
// 生成报价单编号
const quoteNo = 'QT' + new Date().getTime();
// 跳转到详情页
window.location.href = `9-标准报价单详情页.html?quoteNo=${quoteNo}`;
},
handleCancel() {
if (confirm('确定要取消吗?未保存的数据将丢失。')) {
window.location.href = '1-服务价格标准首页.html';
}
}
}
}).mount('#app');
</script>
</body>
</html>
<!DOCTYPE html>
++ /dev/null
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>标准报价单详情 - 服务价格标准化管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 10px;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 25px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.quote-header {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
.quote-info-item {
margin-bottom: 15px;
}
.quote-info-label {
font-size: 14px;
color: #909399;
margin-bottom: 5px;
}
.quote-info-value {
font-size: 16px;
color: #333;
font-weight: 500;
}
.quote-no {
font-size: 20px;
color: #409eff;
font-weight: 600;
}
.detail-section {
margin-bottom: 30px;
}
.detail-section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.detail-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.detail-table th,
.detail-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e4e7ed;
}
.detail-table th {
background: #f5f7fa;
font-weight: 600;
color: #333;
}
.detail-table tr:last-child td {
border-bottom: none;
}
.summary-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.summary-table td {
padding: 12px;
text-align: right;
}
.summary-table td:first-child {
text-align: left;
font-weight: 600;
color: #333;
}
.total-row {
font-weight: 600;
font-size: 18px;
color: #409eff;
border-top: 2px solid #409eff;
}
.total-row td {
padding-top: 15px;
padding-bottom: 15px;
}
.quote-footer {
background: #f5f7fa;
padding: 20px;
border-radius: 6px;
margin-top: 30px;
}
.quote-footer-item {
margin-bottom: 10px;
font-size: 14px;
color: #606266;
}
.quote-footer-item:last-child {
margin-bottom: 0;
}
.quote-footer-label {
font-weight: 600;
color: #333;
}
.btn {
padding: 10px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #409eff;
color: white;
}
.btn-primary:hover {
background: #66b1ff;
}
.btn-success {
background: #67c23a;
color: white;
}
.btn-success:hover {
background: #85ce61;
}
.btn-default {
background: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
.btn-default:hover {
background: #ecf5ff;
color: #409eff;
border-color: #409eff;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: #f0f9ff;
color: #67c23a;
border: 1px solid #67c23a;
}
@media print {
body {
background: white;
padding: 0;
}
.header-actions {
display: none;
}
.card {
box-shadow: none;
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<div class="header-top">
<h1>标准报价单详情</h1>
<div class="header-actions">
<button class="btn btn-success" @click="handleExport">导出</button>
<button class="btn btn-primary" @click="handlePrint">打印</button>
<button class="btn btn-default" @click="handleBack">返回</button>
</div>
</div>
</div>
<div class="card">
<!-- 报价单头部信息 -->
<div class="quote-header">
<div>
<div class="quote-info-item">
<div class="quote-info-label">报价单编号</div>
<div class="quote-info-value quote-no">{{ quoteInfo.quoteNo }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">客户名称</div>
<div class="quote-info-value">{{ quoteInfo.customerName }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">服务内容</div>
<div class="quote-info-value">{{ quoteInfo.serviceDescription }}</div>
</div>
</div>
<div>
<div class="quote-info-item">
<div class="quote-info-label">报价日期</div>
<div class="quote-info-value">{{ quoteInfo.quoteDate }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">报价有效期</div>
<div class="quote-info-value">{{ quoteInfo.validUntil }}</div>
</div>
<div class="quote-info-item">
<div class="quote-info-label">状态</div>
<div class="quote-info-value">
<span class="status-badge active">{{ quoteInfo.status }}</span>
</div>
</div>
</div>
</div>
<!-- 人工费用明细 -->
<div class="detail-section" v-if="laborDetails.length > 0">
<div class="detail-section-title">人工费用明细</div>
<table class="detail-table">
<thead>
<tr>
<th>序号</th>
<th>服务类型</th>
<th>技术等级</th>
<th>计费方式</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in laborDetails" :key="index">
<td>{{ index + 1 }}</td>
<td>{{ item.serviceType }}</td>
<td>{{ item.techLevel }}</td>
<td>{{ item.billingMethod }}</td>
<td>{{ item.quantity }} {{ item.billingMethod === '按小时' ? '小时' : '次' }}</td>
<td>¥{{ item.price }}/{{ item.billingMethod === '按小时' ? '小时' : '次' }}</td>
<td>¥{{ (item.price * item.quantity).toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 零件费用明细 -->
<div class="detail-section" v-if="partDetails.length > 0">
<div class="detail-section-title">零件费用明细</div>
<table class="detail-table">
<thead>
<tr>
<th>序号</th>
<th>零件名称</th>
<th>型号规格</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in partDetails" :key="index">
<td>{{ index + 1 }}</td>
<td>{{ item.partName }}</td>
<td>{{ item.specification }}</td>
<td>{{ item.quantity }}</td>
<td>¥{{ item.price }}</td>
<td>¥{{ (item.price * item.quantity).toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 费用汇总 -->
<div class="detail-section">
<div class="detail-section-title">费用汇总</div>
<table class="summary-table">
<tbody>
<tr v-if="laborSubtotal > 0">
<td>人工费用小计:</td>
<td>¥{{ laborSubtotal.toFixed(2) }}</td>
</tr>
<tr v-if="partSubtotal > 0">
<td>零件费用小计:</td>
<td>¥{{ partSubtotal.toFixed(2) }}</td>
</tr>
<tr class="total-row">
<td>总计(含税):</td>
<td>¥{{ totalAmount.toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 报价依据 -->
<div class="quote-footer">
<div class="quote-footer-item">
<span class="quote-footer-label">报价依据:</span>
服务价格白皮书 {{ quoteInfo.whitebookVersion }}
</div>
<div class="quote-footer-item">
<span class="quote-footer-label">适用规则:</span>
{{ quoteInfo.ruleName }}
</div>
<div class="quote-footer-item">
<span class="quote-footer-label">服务场景:</span>
{{ quoteInfo.scenario }}(质保期外)
</div>
<div class="quote-footer-item">
<span class="quote-footer-label">备注:</span>
{{ quoteInfo.remark || '无' }}
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
// 从URL获取报价单编号
const urlParams = new URLSearchParams(window.location.search);
const quoteNo = urlParams.get('quoteNo') || 'QT' + new Date().getTime();
// 模拟报价单数据(实际应从后端获取)
return {
quoteInfo: {
quoteNo: quoteNo,
customerName: '示例客户有限公司',
serviceDescription: '服务器故障维修服务,包括硬件检测、故障排查、零件更换等',
quoteDate: new Date().toLocaleDateString('zh-CN'),
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString('zh-CN'),
status: '有效',
whitebookVersion: 'V1.0',
ruleName: '停机服务标准报价规则',
scenario: '停机',
remark: '本报价单有效期30天,最终价格以实际服务为准。'
},
laborDetails: [
{
serviceType: '上门',
techLevel: '普通',
billingMethod: '按小时',
price: 150,
quantity: 2
},
{
serviceType: '上门',
techLevel: '高级',
billingMethod: '按小时',
price: 250,
quantity: 1
}
],
partDetails: [
{
partName: '服务器硬盘',
specification: '2TB SATA',
price: 800,
quantity: 1
},
{
partName: '内存条',
specification: '16GB DDR4',
price: 600,
quantity: 2
}
]
}
},
computed: {
laborSubtotal() {
return this.laborDetails.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
partSubtotal() {
return this.partDetails.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
totalAmount() {
return this.laborSubtotal + this.partSubtotal;
}
},
methods: {
handleExport() {
alert('导出功能(占位)\n将导出为PDF或Excel格式');
},
handlePrint() {
window.print();
},
handleBack() {
window.location.href = '8-标准报价单生成页.html';
}
}
}).mount('#app');
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment