Commit 7c1743b7 by suyuchen

feat(vip): 添加VIP客户服务机制相关页面

- 创建VIP客户管理列表页,包含客户信息展示、筛选和状态管理功能 - 实现VIP客户新增编辑页面,支持客户级别和VIP等级配置 - 开发VIP专属负责人配置页,用于设置客户专属服务人员 - 集成Vue.js实现响应式数据绑定和用户交互 - 设计VIP客户统计卡片和数据表格展示 - 添加客户状态切换和负责人分配功能 - 实现本地存储管理客户数据和配置信息
parent d1b7cbd4
<!DOCTYPE html>
<!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>
<!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>
<!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>
<!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>
<!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>
<!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>
<!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>
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