转换Vue3前置
3
.vscode/settings.json
vendored
@@ -41,5 +41,6 @@
|
||||
"typescript.suggest.autoImports": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false
|
||||
"editor.detectIndentation": false,
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
/* eslint-env node */
|
||||
require('eslint-plugin-vue')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
// './.eslintrc-auto-import.json', // 自动导入的全局变量(安装依赖后自动生成)
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-unused-vars': 'warn',
|
||||
'no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
},
|
||||
}
|
||||
|
||||
323
TouchVueThree/PROJECT_STRUCTURE.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# TouchVueThree 项目目录结构说明
|
||||
|
||||
> **PC 端微信客服系统 - Vue3 + TypeScript + Element Plus**
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
TouchVueThree/
|
||||
├── public/ # 静态资源(不会被编译)
|
||||
│ ├── favicon.ico
|
||||
│ └── assets/ # 图片、表情包等静态资源
|
||||
│
|
||||
├── src/
|
||||
│ ├── api/ # API 接口层
|
||||
│ │ ├── request.ts # Axios 封装(拦截器、错误处理)
|
||||
│ │ ├── interceptors.ts # 请求/响应拦截器
|
||||
│ │ └── modules/ # 按业务模块划分的接口
|
||||
│ │ ├── user.ts # 用户相关接口
|
||||
│ │ ├── wechat.ts # 微信相关接口
|
||||
│ │ ├── ai.ts # AI 相关接口
|
||||
│ │ └── common.ts # 通用接口(文件上传等)
|
||||
│ │
|
||||
│ ├── assets/ # 资源文件
|
||||
│ │ ├── images/ # 图片资源
|
||||
│ │ ├── icons/ # 图标资源
|
||||
│ │ └── styles/ # 样式文件
|
||||
│ │ ├── variables.scss # 全局变量(颜色、字体)
|
||||
│ │ ├── mixins.scss # SCSS 混入
|
||||
│ │ ├── reset.scss # 样式重置
|
||||
│ │ └── global.scss # 全局样式
|
||||
│ │
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ ├── common/ # 通用组件(与业务无关)
|
||||
│ │ │ ├── Button/ # 自定义按钮
|
||||
│ │ │ ├── Dialog/ # 自定义对话框
|
||||
│ │ │ ├── Loading/ # 加载组件
|
||||
│ │ │ ├── Empty/ # 空状态
|
||||
│ │ │ └── ErrorBoundary/ # 错误边界
|
||||
│ │ │
|
||||
│ │ └── business/ # 业务组件(微信客服相关)
|
||||
│ │ ├── ChatWindow/ # 聊天窗口
|
||||
│ │ ├── ContactList/ # 联系人列表
|
||||
│ │ ├── SessionList/ # 会话列表
|
||||
│ │ ├── CustomerList/ # 客服账号列表
|
||||
│ │ ├── ProfileCard/ # 客户画像卡片
|
||||
│ │ ├── EmojiPicker/ # 表情选择器
|
||||
│ │ ├── FileUpload/ # 文件上传
|
||||
│ │ └── QuickReply/ # 快捷回复
|
||||
│ │
|
||||
│ ├── composables/ # 组合式函数(Composition API)
|
||||
│ │ ├── core/ # 核心功能
|
||||
│ │ │ ├── useAuth.ts # 认证登录
|
||||
│ │ │ ├── useWebSocket.ts # WebSocket 管理
|
||||
│ │ │ ├── useEventBus.ts # 事件总线
|
||||
│ │ │ └── useRequest.ts # 请求封装
|
||||
│ │ │
|
||||
│ │ └── business/ # 业务功能
|
||||
│ │ ├── useMessages.ts # 消息管理
|
||||
│ │ ├── useContacts.ts # 联系人管理
|
||||
│ │ ├── useSessions.ts # 会话管理
|
||||
│ │ ├── useAi.ts # AI 功能
|
||||
│ │ └── useUpload.ts # 文件上传
|
||||
│ │
|
||||
│ ├── directives/ # 自定义指令
|
||||
│ │ ├── vLoading.ts # 加载指令
|
||||
│ │ ├── vLazyLoad.ts # 懒加载指令
|
||||
│ │ └── vPermission.ts # 权限指令
|
||||
│ │
|
||||
│ ├── layouts/ # 页面布局
|
||||
│ │ ├── DefaultLayout.vue # 默认布局
|
||||
│ │ └── ChatLayout.vue # 聊天布局(三栏)
|
||||
│ │
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ ├── index.ts # 路由主文件
|
||||
│ │ ├── guards.ts # 路由守卫(权限控制)
|
||||
│ │ └── routes.ts # 路由配置
|
||||
│ │
|
||||
│ ├── stores/ # Pinia 状态管理
|
||||
│ │ ├── index.ts # Store 统一出口
|
||||
│ │ └── modules/ # Store 模块
|
||||
│ │ ├── user.ts # 用户状态
|
||||
│ │ ├── app.ts # 应用全局状态
|
||||
│ │ ├── websocket.ts # WebSocket 状态
|
||||
│ │ └── wechat/ # 微信模块
|
||||
│ │ ├── contacts.ts # 联系人
|
||||
│ │ ├── messages.ts # 消息
|
||||
│ │ ├── sessions.ts # 会话
|
||||
│ │ └── ai.ts # AI
|
||||
│ │
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ │ ├── global.d.ts # 全局类型
|
||||
│ │ ├── api.d.ts # API 类型
|
||||
│ │ ├── user.ts # 用户类型
|
||||
│ │ ├── wechat.ts # 微信类型
|
||||
│ │ └── websocket.ts # WebSocket 类型
|
||||
│ │
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── common.ts # 通用工具
|
||||
│ │ ├── date.ts # 日期处理
|
||||
│ │ ├── format.ts # 格式化
|
||||
│ │ ├── validator.ts # 验证
|
||||
│ │ ├── storage.ts # 本地存储
|
||||
│ │ ├── event-bus.ts # 事件总线
|
||||
│ │ └── sentry/ # 错误监控
|
||||
│ │ └── index.ts
|
||||
│ │
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── Login/ # 登录页
|
||||
│ │ │ └── index.vue
|
||||
│ │ │
|
||||
│ │ ├── Chat/ # 聊天主页(核心功能)
|
||||
│ │ │ ├── index.vue
|
||||
│ │ │ └── components/ # 聊天页专用组件
|
||||
│ │ │ ├── Sidebar.vue # 左侧边栏
|
||||
│ │ │ ├── Main.vue # 中间聊天区
|
||||
│ │ │ └── Panel.vue # 右侧面板
|
||||
│ │ │
|
||||
│ │ ├── Dashboard/ # 数据看板
|
||||
│ │ │ └── index.vue
|
||||
│ │ │
|
||||
│ │ ├── Settings/ # 系统设置
|
||||
│ │ │ └── index.vue
|
||||
│ │ │
|
||||
│ │ ├── PowerCenter/ # 能力中心
|
||||
│ │ │ ├── CustomerManagement/ # 客户管理
|
||||
│ │ │ ├── ContentManagement/ # 内容管理
|
||||
│ │ │ ├── DataStatistics/ # 数据统计
|
||||
│ │ │ └── AiTraining/ # AI 训练
|
||||
│ │ │
|
||||
│ │ └── 404/ # 404 页面
|
||||
│ │ └── index.vue
|
||||
│ │
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.ts # 入口文件
|
||||
│
|
||||
├── .env.development # 开发环境变量
|
||||
├── .env.production # 生产环境变量
|
||||
├── .eslintrc.cjs # ESLint 配置
|
||||
├── .prettierrc # Prettier 配置
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
├── vite.config.ts # Vite 配置
|
||||
└── package.json # 项目配置
|
||||
```
|
||||
|
||||
## 🎯 命名规范
|
||||
|
||||
### 文件命名
|
||||
|
||||
- **组件文件**:PascalCase(大驼峰)
|
||||
- `ChatWindow.vue`, `MessageList.vue`
|
||||
|
||||
- **工具文件**:camelCase(小驼峰)
|
||||
- `common.ts`, `useAuth.ts`
|
||||
|
||||
- **类型文件**:camelCase 或 kebab-case
|
||||
- `user.ts`, `api.d.ts`
|
||||
|
||||
### 目录命名
|
||||
|
||||
- **kebab-case**(短横线)或 **camelCase**
|
||||
- `components/business`, `composables/core`
|
||||
|
||||
### 组件命名规则
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐
|
||||
<script setup lang="ts" name="ChatWindow">
|
||||
// 组件逻辑
|
||||
</script>
|
||||
|
||||
// ❌ 避免
|
||||
<script setup lang="ts">
|
||||
// 没有 name 属性
|
||||
</script>
|
||||
```
|
||||
|
||||
## 🔧 开发规范
|
||||
|
||||
### 1. Composables 使用规范
|
||||
|
||||
```typescript
|
||||
// composables/core/useAuth.ts
|
||||
import { ref, computed } from 'vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
export function useAuth() {
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isLoggedIn = computed(() => userStore.isLoggedIn)
|
||||
|
||||
const login = async (credentials) => {
|
||||
// 登录逻辑
|
||||
}
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
login
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Store 使用规范
|
||||
|
||||
```typescript
|
||||
// stores/modules/user.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const user = ref(null)
|
||||
const token = ref('')
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
// Actions
|
||||
const setUser = (userData) => {
|
||||
user.value = userData
|
||||
}
|
||||
|
||||
return { user, token, isLoggedIn, setUser }
|
||||
}, {
|
||||
persist: {
|
||||
key: 'user-store',
|
||||
paths: ['user', 'token']
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. API 调用规范
|
||||
|
||||
```typescript
|
||||
// api/modules/user.ts
|
||||
import request from '../request'
|
||||
|
||||
export const loginApi = (data: LoginParams) => {
|
||||
return request('/auth/login', data, 'POST')
|
||||
}
|
||||
|
||||
export const getUserInfoApi = () => {
|
||||
return request('/user/info', {}, 'GET')
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 组件引入规范
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. Vue API
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// 2. 第三方库
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 3. Store
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
// 4. Composables
|
||||
import { useAuth } from '@/composables/core/useAuth'
|
||||
|
||||
// 5. 类型
|
||||
import type { User } from '@/types/user'
|
||||
|
||||
// 6. 组件
|
||||
import ChatWindow from '@/components/business/ChatWindow/index.vue'
|
||||
</script>
|
||||
```
|
||||
|
||||
## 📝 注释规范
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 用户登录
|
||||
* @param credentials 登录凭证
|
||||
* @returns Promise<UserInfo>
|
||||
*/
|
||||
export async function login(credentials: LoginParams): Promise<UserInfo> {
|
||||
// 实现逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 启动命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
|
||||
# 类型检查
|
||||
pnpm type-check
|
||||
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
|
||||
# 格式化代码
|
||||
pnpm format
|
||||
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
|
||||
# 预览生产版本
|
||||
pnpm preview
|
||||
|
||||
# 打包分析
|
||||
pnpm analyze
|
||||
```
|
||||
|
||||
## 📌 下一步计划
|
||||
|
||||
1. ✅ 目录结构已创建
|
||||
2. ✅ package.json 已优化
|
||||
3. ⏳ 安装依赖:`pnpm install`
|
||||
4. ⏳ 创建基础配置文件
|
||||
5. ⏳ 开发核心功能
|
||||
|
||||
---
|
||||
|
||||
**更新时间**: 2026-01-12
|
||||
**版本**: 2.0.0
|
||||
**技术栈**: Vue 3.4 + TypeScript 5.4 + Element Plus 2.5 + Pinia 2.1
|
||||
@@ -20,6 +20,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"strict": false,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vite/client"],
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.js", "src/**/*.vue", "env.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,27 +1,56 @@
|
||||
{
|
||||
"name": "touchvue-three",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.ts,.tsx --fix --ignore-path .gitignore",
|
||||
"lint:check": "eslint . --ext .vue,.ts,.tsx",
|
||||
"format": "prettier --write \"src/**/*.{vue,ts,tsx,js,json,scss,css}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{vue,ts,tsx,js,json,scss,css}\"",
|
||||
"analyze": "vite build --mode analyze"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21"
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"element-plus": "^2.5.6",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.6.7",
|
||||
"@tanstack/vue-query": "^5.20.0",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"vue-echarts": "^6.6.8",
|
||||
"@sentry/vue": "^7.100.0",
|
||||
"mitt": "^3.0.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"vite": "^5.1.4",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"typescript": "^5.4.5",
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"sass": "^1.75.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"postcss": "^8.4.35",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.1.4"
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
5102
TouchVueThree/pnpm-lock.yaml
generated
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,86 +1,84 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex h-screen w-screen bg-[#F8FAFC] text-slate-700 overflow-hidden font-sans"
|
||||
>
|
||||
<!-- 全局侧边栏 -->
|
||||
<Sidebar
|
||||
:current-tab="currentTab"
|
||||
@change-tab="handleTabChange"
|
||||
@logo-click="handleLogoClick"
|
||||
@add-account="handleAddAccount"
|
||||
@open-settings="handleOpenSettings"
|
||||
@select-account="handleSelectAccount"
|
||||
/>
|
||||
<div class="app-container">
|
||||
<div class="content">
|
||||
<h1>Hello TouchVueThree</h1>
|
||||
<p class="subtitle">Vue 3 + TypeScript + Element Plus</p>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="flex-1 relative overflow-hidden">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<component :is="activeComponent" />
|
||||
</Transition>
|
||||
</main>
|
||||
<!-- Element Plus 示例组件 -->
|
||||
<div class="demo-section">
|
||||
<el-button type="primary" @click="handleClick">主要按钮</el-button>
|
||||
<el-button type="success">成功按钮</el-button>
|
||||
<el-button type="info">信息按钮</el-button>
|
||||
<el-button type="warning">警告按钮</el-button>
|
||||
<el-button type="danger">危险按钮</el-button>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<el-input v-model="inputValue" placeholder="请输入内容" clearable />
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<el-tag>标签一</el-tag>
|
||||
<el-tag type="success">标签二</el-tag>
|
||||
<el-tag type="info">标签三</el-tag>
|
||||
<el-tag type="warning">标签四</el-tag>
|
||||
<el-tag type="danger">标签五</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Sidebar from './components/Sidebar.vue'
|
||||
import ChatIndex from './views/Chat/Index.vue'
|
||||
import Contacts from './views/Contacts.vue'
|
||||
import Analytics from './views/Analytics.vue'
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const currentTab = ref('chat')
|
||||
// TypeScript 类型定义
|
||||
const inputValue = ref<string>('')
|
||||
|
||||
const activeComponent = computed(() => {
|
||||
switch (currentTab.value) {
|
||||
case 'chat':
|
||||
return ChatIndex
|
||||
case 'contacts':
|
||||
return Contacts // 通讯录页面
|
||||
case 'analytics':
|
||||
return Analytics
|
||||
default:
|
||||
return ChatIndex
|
||||
}
|
||||
})
|
||||
|
||||
// 处理标签切换
|
||||
const handleTabChange = (tab) => {
|
||||
currentTab.value = tab
|
||||
}
|
||||
|
||||
// 处理 Logo 点击
|
||||
const handleLogoClick = () => {
|
||||
// 可以跳转到首页或执行其他操作
|
||||
currentTab.value = 'chat'
|
||||
}
|
||||
|
||||
// 处理添加账号
|
||||
const handleAddAccount = () => {
|
||||
// TODO: 打开添加账号对话框或跳转到账号管理页面
|
||||
console.log('添加账号')
|
||||
currentTab.value = 'contacts'
|
||||
}
|
||||
|
||||
// 处理打开设置
|
||||
const handleOpenSettings = () => {
|
||||
// TODO: 打开设置对话框或跳转到设置页面
|
||||
console.log('打开设置')
|
||||
}
|
||||
|
||||
// 处理选择账号
|
||||
const handleSelectAccount = (account) => {
|
||||
// TODO: 切换到选中的账号
|
||||
console.log('选择账号:', account)
|
||||
// 事件处理函数
|
||||
const handleClick = (): void => {
|
||||
ElMessage({
|
||||
message: '恭喜你,转换成功!🎉',
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 60px 80px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: 30px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
11
TouchVueThree/src/assets/element-variables.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
// Element Plus 主题变量覆盖(可选)
|
||||
// 更多变量请参考: https://element-plus.org/zh-CN/guide/theming.html
|
||||
|
||||
// 主题色
|
||||
// $--el-color-primary: #409eff;
|
||||
|
||||
// 边框圆角
|
||||
// $--el-border-radius-base: 4px;
|
||||
|
||||
// 字体
|
||||
// $--el-font-size-base: 14px;
|
||||
@@ -1,28 +1,87 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* 全局样式 - Vue 3 + TypeScript + Element Plus */
|
||||
|
||||
/* 自定义样式 */
|
||||
body {
|
||||
font-family: "Plus Jakarta Sans", "Inter", sans-serif;
|
||||
/* CSS 重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 全局字体 */
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条 */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面切换动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
/* 自定义滚动条 */
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #dcdfe6;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: #c0c4cc;
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar-track {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
/* 页面切换动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
.mobile-h-screen {
|
||||
height: 100vh;
|
||||
height: 100dvh; /* 解决移动端地址栏遮挡问题 */
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
.pb-safe {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Element Plus 全局样式覆盖 */
|
||||
:root {
|
||||
--el-color-primary: #409eff;
|
||||
--el-border-radius-base: 4px;
|
||||
}
|
||||
|
||||
/* 响应式工具类 */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.desktop-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<div class="group relative">
|
||||
<!-- 胶囊主体 -->
|
||||
<div
|
||||
class="flex items-center gap-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white pl-4 pr-1 py-1 rounded-full shadow-md cursor-pointer hover:shadow-lg hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
<div class="flex flex-col leading-none items-end">
|
||||
<span class="text-[9px] font-bold opacity-80 uppercase tracking-wide"
|
||||
>AI 算力</span
|
||||
>
|
||||
<span class="text-sm font-bold font-mono">{{ animatedBalance }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="w-8 h-8 bg-white text-indigo-600 rounded-full flex items-center justify-center shadow-inner"
|
||||
>
|
||||
<i
|
||||
class="fa-solid fa-bolt text-sm"
|
||||
:class="{ 'animate-pulse': isAnimating }"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 悬浮详情 (Dropdown) -->
|
||||
<div
|
||||
class="absolute top-full right-0 mt-2 w-64 bg-white rounded-xl shadow-xl border border-gray-100 p-4 hidden group-hover:block z-50"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-xs font-bold text-gray-700">算力钱包</span>
|
||||
<span
|
||||
class="text-[10px] bg-green-100 text-green-700 px-2 py-0.5 rounded-full"
|
||||
>状态正常</span
|
||||
>
|
||||
</div>
|
||||
<div class="h-2 bg-gray-100 rounded-full mb-2 overflow-hidden">
|
||||
<div class="h-full bg-indigo-500 w-3/4 rounded-full"></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mb-3">本月额度剩余 75%</div>
|
||||
<button
|
||||
class="w-full py-1.5 bg-slate-800 text-white text-xs rounded-lg hover:bg-slate-700 transition"
|
||||
>
|
||||
立即充值
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
balance: { type: Number, default: 0 },
|
||||
})
|
||||
|
||||
const isAnimating = ref(false)
|
||||
|
||||
const animatedBalance = computed(() => props.balance.toLocaleString())
|
||||
|
||||
// 暴露给父组件调用的方法:播放扣费动画
|
||||
const playDeductAnimation = () => {
|
||||
isAnimating.value = true
|
||||
setTimeout(() => (isAnimating.value = false), 500)
|
||||
}
|
||||
|
||||
defineExpose({ playDeductAnimation })
|
||||
</script>
|
||||
@@ -1,183 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-16 bg-slate-900 flex flex-col items-center py-6 flex-shrink-0 z-20"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<div
|
||||
class="w-10 h-10 bg-indigo-500 rounded-lg flex items-center justify-center text-white font-bold text-xl mb-8 shadow-lg cursor-pointer hover:scale-105 transition-transform"
|
||||
@click="$emit('logo-click')"
|
||||
>
|
||||
<i class="fa-solid fa-robot"></i>
|
||||
</div>
|
||||
|
||||
<!-- 功能模块 -->
|
||||
<div class="space-y-6 flex flex-col items-center w-full">
|
||||
<!-- 聚合聊天 -->
|
||||
<div
|
||||
@click="$emit('change-tab', 'chat')"
|
||||
class="group relative cursor-pointer"
|
||||
:class="
|
||||
currentTab === 'chat'
|
||||
? 'text-indigo-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
"
|
||||
>
|
||||
<i class="fa-solid fa-comments text-2xl transition-colors"></i>
|
||||
<span
|
||||
class="absolute left-14 bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition whitespace-nowrap z-50 pointer-events-none"
|
||||
>
|
||||
聚合聊天
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 通讯录 -->
|
||||
<div
|
||||
@click="$emit('change-tab', 'contacts')"
|
||||
class="group relative cursor-pointer transition-colors"
|
||||
:class="
|
||||
currentTab === 'contacts'
|
||||
? 'text-indigo-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
"
|
||||
>
|
||||
<i class="fa-solid fa-address-book text-xl"></i>
|
||||
<span
|
||||
class="absolute left-14 bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition whitespace-nowrap z-50 pointer-events-none"
|
||||
>
|
||||
通讯录
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 数据看板 -->
|
||||
<div
|
||||
@click="$emit('change-tab', 'analytics')"
|
||||
class="group relative cursor-pointer transition-colors"
|
||||
:class="
|
||||
currentTab === 'analytics'
|
||||
? 'text-indigo-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
"
|
||||
>
|
||||
<i class="fa-solid fa-chart-pie text-xl"></i>
|
||||
<span
|
||||
class="absolute left-14 bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition whitespace-nowrap z-50 pointer-events-none"
|
||||
>
|
||||
数据看板
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="mt-8 mb-4 w-8 border-b border-gray-700"></div>
|
||||
|
||||
<!-- 多账号头像 (模拟微信多开) -->
|
||||
<div
|
||||
class="space-y-4 flex flex-col items-center overflow-y-auto no-scrollbar flex-1 min-h-0"
|
||||
>
|
||||
<div
|
||||
v-for="account in accounts"
|
||||
:key="account.id"
|
||||
@click="selectAccount(account.id)"
|
||||
class="relative cursor-pointer group"
|
||||
>
|
||||
<img
|
||||
:src="account.avatar"
|
||||
:class="[
|
||||
'w-10 h-10 rounded-full border-2 object-cover transition-all',
|
||||
account.active
|
||||
? 'border-green-500 opacity-100'
|
||||
: 'border-transparent hover:border-gray-500 opacity-60 hover:opacity-100',
|
||||
]"
|
||||
:alt="account.name"
|
||||
/>
|
||||
<!-- 未读消息红点 -->
|
||||
<div
|
||||
v-if="account.hasNotification"
|
||||
class="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-slate-900"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 添加账号按钮 -->
|
||||
<div
|
||||
@click="$emit('add-account')"
|
||||
class="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center text-gray-400 hover:text-white cursor-pointer hover:bg-gray-700 transition"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部设置 -->
|
||||
<div class="mt-auto pb-4">
|
||||
<div
|
||||
@click="$emit('open-settings')"
|
||||
class="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-gray-400 hover:text-white cursor-pointer transition-colors"
|
||||
>
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
currentTab: {
|
||||
type: String,
|
||||
default: 'chat',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'change-tab',
|
||||
'logo-click',
|
||||
'add-account',
|
||||
'open-settings',
|
||||
'select-account',
|
||||
])
|
||||
|
||||
// 账号列表数据
|
||||
const accounts = ref([
|
||||
{
|
||||
id: 'wx1',
|
||||
name: '账号1',
|
||||
avatar: 'https://i.pravatar.cc/150?u=wx1',
|
||||
active: true,
|
||||
hasNotification: true,
|
||||
},
|
||||
{
|
||||
id: 'wx2',
|
||||
name: '账号2',
|
||||
avatar: 'https://i.pravatar.cc/150?u=wx2',
|
||||
active: false,
|
||||
hasNotification: false,
|
||||
},
|
||||
])
|
||||
|
||||
// 选择账号
|
||||
const selectAccount = (accountId) => {
|
||||
// 更新激活状态
|
||||
accounts.value.forEach((account) => {
|
||||
account.active = account.id === accountId
|
||||
})
|
||||
// 触发事件
|
||||
const account = accounts.value.find((acc) => acc.id === accountId)
|
||||
if (account) {
|
||||
// 可以在这里清除通知
|
||||
account.hasNotification = false
|
||||
// 触发选择账号事件
|
||||
emit('select-account', account)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条隐藏 */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
21
TouchVueThree/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
// Element Plus 全局类型
|
||||
declare module 'element-plus/global'
|
||||
|
||||
// 环境变量类型声明
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_TITLE: string
|
||||
readonly VITE_API_BASE_URL: string
|
||||
// 更多环境变量...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './assets/style.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.mount('#app')
|
||||
18
TouchVueThree/src/main.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './assets/style.css'
|
||||
|
||||
// Element Plus 样式(已配置自动导入,无需手动引入组件)
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
// Element Plus 图标(可选,按需导入)
|
||||
// import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标(可选)
|
||||
// for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
// app.component(key, component)
|
||||
// }
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<div class="p-8 h-full overflow-y-auto custom-scroll">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h2 class="text-2xl font-bold text-slate-800">聚合账号管理</h2>
|
||||
<button
|
||||
class="bg-primary text-white px-6 py-2.5 rounded-xl font-bold shadow-lg shadow-indigo-200 flex items-center gap-2"
|
||||
>
|
||||
<i class="fa-solid fa-qrcode"></i> 扫码接入
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div
|
||||
class="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm hover:shadow-md transition-all relative overflow-hidden group"
|
||||
>
|
||||
<span
|
||||
class="absolute top-3 right-3 bg-green-100 text-green-700 text-xs px-2 py-1 rounded-lg font-bold"
|
||||
>在线</span
|
||||
>
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=wx1"
|
||||
class="w-16 h-16 rounded-2xl border-2 border-gray-100"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg text-slate-800">商务-大刘</h3>
|
||||
<p class="text-xs text-gray-400">ID: business_01</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div class="p-8 h-full overflow-y-auto custom-scroll">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<h2 class="text-2xl font-bold text-slate-800 mb-6">AI 效能看板</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div
|
||||
class="bg-gradient-to-br from-[#4F46E5] to-[#7C3AED] rounded-2xl p-6 text-white shadow-lg relative overflow-hidden"
|
||||
>
|
||||
<div class="relative z-10">
|
||||
<div class="text-indigo-100 text-sm font-medium mb-1">
|
||||
AI 算力余额
|
||||
</div>
|
||||
<div class="text-4xl font-bold font-mono tracking-tight mb-4">
|
||||
12,450
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm">
|
||||
<h3 class="font-bold text-slate-800 mb-6">近7天接待趋势</h3>
|
||||
<div class="h-64 flex items-end justify-between gap-2 px-2">
|
||||
<div
|
||||
v-for="h in [20, 30, 45, 25, 60, 40, 15]"
|
||||
:key="h"
|
||||
class="w-full flex flex-col gap-1 items-center group"
|
||||
>
|
||||
<div
|
||||
:style="{ height: h + 'px' }"
|
||||
class="w-4 bg-purple-400 rounded-t-sm opacity-80 group-hover:opacity-100 transition-all"
|
||||
></div>
|
||||
<div
|
||||
:style="{ height: h / 2 + 'px' }"
|
||||
class="w-4 bg-indigo-500 rounded-b-sm opacity-80 group-hover:opacity-100 transition-all"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white border-l border-gray-100 flex flex-col flex-shrink-0 transition-all duration-300 overflow-hidden"
|
||||
:style="{ width: visible ? '300px' : '0', opacity: visible ? '1' : '0', pointerEvents: visible ? 'auto' : 'none' }"
|
||||
>
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex p-1 bg-gray-50 m-4 rounded-lg border border-gray-100">
|
||||
<button
|
||||
@click="tab = 'profile'"
|
||||
class="flex-1 py-1.5 text-xs font-bold rounded-md transition"
|
||||
:class="
|
||||
tab === 'profile'
|
||||
? 'bg-white text-indigo-600 shadow-sm'
|
||||
: 'text-gray-500'
|
||||
"
|
||||
>
|
||||
资料
|
||||
</button>
|
||||
<button
|
||||
@click="tab = 'scripts'"
|
||||
class="flex-1 py-1.5 text-xs font-bold rounded-md transition"
|
||||
:class="
|
||||
tab === 'scripts'
|
||||
? 'bg-white text-indigo-600 shadow-sm'
|
||||
: 'text-gray-500'
|
||||
"
|
||||
>
|
||||
话术
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-5 pb-5 custom-scroll">
|
||||
<!-- 资料内容 -->
|
||||
<div v-if="tab === 'profile'" class="space-y-6">
|
||||
<div class="text-center">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=8"
|
||||
class="w-20 h-20 rounded-full border-4 border-white shadow-lg mx-auto"
|
||||
/>
|
||||
<h3 class="mt-2 font-bold text-slate-800">大野</h3>
|
||||
<p class="text-xs text-gray-400">ID: ys666wow</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-xs font-bold text-gray-400 uppercase mb-2">标签</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
class="px-2 py-1 bg-red-50 text-red-600 text-xs rounded border border-red-100"
|
||||
>高意向</span
|
||||
>
|
||||
<span
|
||||
class="px-2 py-1 bg-blue-50 text-blue-600 text-xs rounded border border-blue-100"
|
||||
>游戏玩家</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-yellow-50 p-3 rounded-lg border border-yellow-100 text-xs text-yellow-700"
|
||||
>
|
||||
<strong>跟进提醒:</strong><br />明天上午10点确认订单情况。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 话术内容 -->
|
||||
<div v-else class="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索话术..."
|
||||
class="w-full bg-gray-50 text-xs py-2 px-3 rounded-lg border border-gray-200 mb-2"
|
||||
/>
|
||||
<div
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="p-3 bg-white border border-gray-100 rounded-xl hover:border-indigo-200 cursor-pointer group"
|
||||
>
|
||||
<h5
|
||||
class="text-xs font-bold text-slate-700 mb-1 group-hover:text-indigo-600"
|
||||
>
|
||||
通用开场 {{ i }}
|
||||
</h5>
|
||||
<p class="text-xs text-gray-500 truncate">
|
||||
您好,我是您的专属顾问,很高兴...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const props = defineProps({ visible: Boolean, chat: Object })
|
||||
const tab = ref('profile')
|
||||
</script>
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div class="w-80 border-r border-gray-100 flex flex-col bg-white">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="p-4 border-b border-gray-50">
|
||||
<h2 class="font-bold text-lg text-slate-800 mb-3">消息 (12)</h2>
|
||||
<div class="relative">
|
||||
<i
|
||||
class="fa-solid fa-magnifying-glass absolute left-3 top-3 text-gray-400 text-xs"
|
||||
></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索联系人..."
|
||||
class="w-full bg-gray-50 text-sm pl-9 pr-4 py-2 rounded-lg border-transparent focus:bg-white focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
<!-- 快捷标签 -->
|
||||
<div class="flex gap-2 mt-3 overflow-x-auto no-scrollbar pb-1">
|
||||
<span
|
||||
class="px-3 py-1 bg-indigo-50 text-indigo-600 text-xs font-bold rounded-md cursor-pointer whitespace-nowrap"
|
||||
>全部</span
|
||||
>
|
||||
<span
|
||||
class="px-3 py-1 bg-white border border-gray-200 text-gray-500 hover:bg-gray-50 text-xs rounded-md cursor-pointer whitespace-nowrap"
|
||||
>未读</span
|
||||
>
|
||||
<span
|
||||
class="px-3 py-1 bg-purple-50 text-purple-600 border border-purple-100 text-xs rounded-md cursor-pointer whitespace-nowrap flex items-center gap-1"
|
||||
>
|
||||
<i class="fa-solid fa-robot"></i> AI托管
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="flex-1 overflow-y-auto custom-scroll">
|
||||
<div
|
||||
v-for="chat in chats"
|
||||
:key="chat.id"
|
||||
@click="$emit('select', chat.id)"
|
||||
class="p-3 mx-2 my-1 rounded-xl cursor-pointer transition-all border flex gap-3 items-center"
|
||||
:class="
|
||||
activeId === chat.id
|
||||
? 'bg-indigo-50 border-indigo-100'
|
||||
: 'hover:bg-gray-50 border-transparent'
|
||||
"
|
||||
>
|
||||
<div class="relative">
|
||||
<img :src="chat.avatar" class="w-11 h-11 rounded-lg object-cover" />
|
||||
<div
|
||||
v-if="chat.unread"
|
||||
class="absolute -top-1.5 -right-1.5 bg-red-500 text-white text-[10px] h-4 min-w-[16px] px-1 flex items-center justify-center rounded-full border border-white"
|
||||
>
|
||||
{{ chat.unread }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-baseline mb-1">
|
||||
<h3 class="font-bold text-sm text-slate-700 truncate">
|
||||
{{ chat.name }}
|
||||
</h3>
|
||||
<span class="text-[10px] text-gray-400">{{ chat.time }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
v-if="chat.isAi"
|
||||
class="bg-purple-100 text-purple-600 text-[9px] px-1 rounded font-bold"
|
||||
>AI</span
|
||||
>
|
||||
<p class="text-xs text-gray-400 truncate">{{ chat.lastMsg }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps(['activeId'])
|
||||
defineEmits(['select'])
|
||||
|
||||
const chats = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '大野(朋友圈业务)',
|
||||
avatar: 'https://i.pravatar.cc/150?u=8',
|
||||
time: '11:45',
|
||||
lastMsg: '已为您生成3条回复建议...',
|
||||
unread: 0,
|
||||
isAi: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '京东内购福利群',
|
||||
avatar: 'https://i.pravatar.cc/150?u=group',
|
||||
time: '10:20',
|
||||
lastMsg: '张三: 谁有券?',
|
||||
unread: 12,
|
||||
isAi: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'VIP客户-王总',
|
||||
avatar: 'https://i.pravatar.cc/150?u=ceo',
|
||||
time: '昨天',
|
||||
lastMsg: '合同已确认',
|
||||
unread: 0,
|
||||
isAi: false,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -1,275 +0,0 @@
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col min-w-0 bg-[#F8FAFC] relative">
|
||||
<!-- Header (关键修改:功能按钮 + 算力展示) -->
|
||||
<header
|
||||
class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shadow-sm z-10"
|
||||
>
|
||||
<!-- Left: Chat Info -->
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="font-bold text-lg text-gray-800">大野(朋友圈业务)</h2>
|
||||
<span
|
||||
class="px-2 py-0.5 bg-green-50 text-green-600 text-xs rounded border border-green-200 flex items-center gap-1"
|
||||
>
|
||||
<span
|
||||
class="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"
|
||||
></span>
|
||||
微信在线
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Right: Functions & AI Credits -->
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Functional Buttons (Clean Style) -->
|
||||
<div class="flex items-center gap-2 border-r border-gray-200 pr-6">
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 hover:text-indigo-600 hover:bg-indigo-50 rounded transition"
|
||||
title="发朋友圈"
|
||||
@click="$emit('post-moments')"
|
||||
>
|
||||
<i class="fa-brands fa-weixin"></i>
|
||||
<span class="hidden xl:inline">发朋友圈</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 hover:text-indigo-600 hover:bg-indigo-50 rounded transition"
|
||||
title="同步消息"
|
||||
@click="$emit('sync-messages')"
|
||||
>
|
||||
<i class="fa-solid fa-arrows-rotate"></i>
|
||||
<span class="hidden xl:inline">同步</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 hover:text-indigo-600 hover:bg-indigo-50 rounded transition"
|
||||
title="创建任务"
|
||||
@click="$emit('create-task')"
|
||||
>
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 算力胶囊 -->
|
||||
<AICreditPill ref="creditPill" :balance="balance" />
|
||||
|
||||
<!-- Toggle Sidebar Info -->
|
||||
<button
|
||||
@click="$emit('toggle-detail')"
|
||||
class="text-gray-400 hover:text-indigo-600 transition ml-2"
|
||||
>
|
||||
<i class="fa-solid fa-circle-info text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 消息流 -->
|
||||
<div
|
||||
class="flex-1 overflow-y-auto p-6 space-y-6 custom-scroll"
|
||||
ref="scrollContainer"
|
||||
>
|
||||
<div class="text-center">
|
||||
<span
|
||||
class="text-[10px] text-gray-400 bg-gray-100 px-3 py-1 rounded-full"
|
||||
>今天 10:23</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div
|
||||
v-for="(msg, i) in messages"
|
||||
:key="i"
|
||||
class="flex gap-3 items-end group"
|
||||
:class="{ 'flex-row-reverse': msg.isMe }"
|
||||
>
|
||||
<img
|
||||
:src="
|
||||
msg.isMe
|
||||
? 'https://i.pravatar.cc/150?u=wx1'
|
||||
: 'https://i.pravatar.cc/150?u=8'
|
||||
"
|
||||
class="w-8 h-8 rounded-lg shadow-sm"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-1 max-w-[70%]"
|
||||
:class="{ 'items-end': msg.isMe }"
|
||||
>
|
||||
<span class="text-[10px] text-gray-400 mx-1">{{
|
||||
msg.isMe ? '我' : '大野'
|
||||
}}</span>
|
||||
|
||||
<!-- AI 标识 -->
|
||||
<div v-if="msg.isAi" class="flex items-center gap-1 mb-0.5">
|
||||
<span
|
||||
class="text-[9px] bg-purple-50 text-purple-600 border border-purple-100 px-1 rounded"
|
||||
>AI 生成</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-3 rounded-2xl text-sm leading-relaxed shadow-sm relative"
|
||||
:class="
|
||||
msg.isMe
|
||||
? 'bg-indigo-600 text-white rounded-br-none'
|
||||
: 'bg-white text-slate-700 border border-gray-100 rounded-bl-none'
|
||||
"
|
||||
>
|
||||
{{ msg.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 建议卡片 (Interactive) -->
|
||||
<div v-if="showAiCard" class="flex justify-end pr-12 animate-fade-in-up">
|
||||
<div
|
||||
class="bg-gradient-to-br from-purple-50 to-white border border-purple-100 rounded-xl p-4 shadow-lg max-w-md relative overflow-hidden"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div
|
||||
class="flex items-center gap-2 text-purple-600 font-bold text-xs"
|
||||
>
|
||||
<i class="fa-solid fa-sparkles"></i> AI 建议回复
|
||||
</div>
|
||||
<span
|
||||
class="text-[10px] text-orange-500 bg-orange-50 px-2 py-0.5 rounded border border-orange-100"
|
||||
>预估消耗 5 算力</span
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm text-slate-600 mb-3 bg-white/50 p-2 rounded border border-purple-50 border-dashed"
|
||||
>
|
||||
“亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。”
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
@click="showAiCard = false"
|
||||
class="px-3 py-1.5 text-xs text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
忽略
|
||||
</button>
|
||||
<button
|
||||
@click="useAiSuggestion"
|
||||
class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs rounded-lg shadow-sm transition flex items-center gap-2"
|
||||
>
|
||||
<i class="fa-solid fa-paper-plane"></i> 发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="p-4 bg-white border-t border-gray-100 z-10">
|
||||
<div class="flex justify-between items-center mb-2 px-1">
|
||||
<div class="flex gap-4 text-gray-400">
|
||||
<i
|
||||
class="fa-regular fa-face-smile hover:text-indigo-600 cursor-pointer"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-folder-open hover:text-indigo-600 cursor-pointer"
|
||||
></i>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 cursor-pointer"
|
||||
@click="aiMode = !aiMode"
|
||||
>
|
||||
<span class="text-xs font-bold text-slate-500">AI 辅助</span>
|
||||
<div
|
||||
class="w-8 h-4 bg-gray-200 rounded-full relative transition-colors"
|
||||
:class="{ 'bg-indigo-600': aiMode }"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 bg-white rounded-full absolute top-0.5 transition-all shadow-sm"
|
||||
:class="aiMode ? 'right-0.5' : 'left-0.5'"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<textarea
|
||||
v-model="inputMsg"
|
||||
class="w-full h-20 bg-gray-50 border border-gray-200 rounded-xl p-3 text-sm focus:bg-white focus:border-indigo-500 focus:outline-none resize-none transition"
|
||||
placeholder="输入消息..."
|
||||
></textarea>
|
||||
<button
|
||||
@click="sendMsg"
|
||||
class="absolute bottom-3 right-3 bg-slate-900 text-white px-4 py-1.5 rounded-lg text-xs font-bold hover:bg-slate-700 transition"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
import AICreditPill from '../../components/AICreditPill.vue'
|
||||
|
||||
defineEmits([
|
||||
'toggle-detail',
|
||||
'post-moments',
|
||||
'sync-messages',
|
||||
'create-task',
|
||||
])
|
||||
|
||||
const creditPill = ref(null)
|
||||
const scrollContainer = ref(null)
|
||||
const balance = ref(12450)
|
||||
const showAiCard = ref(true)
|
||||
const aiMode = ref(true)
|
||||
const inputMsg = ref('')
|
||||
|
||||
const messages = ref([
|
||||
{
|
||||
text: '老板,上次说的那个魔兽代练套餐,如果是老客户的话,还能再送一次幻化吗?',
|
||||
isMe: false,
|
||||
isAi: false,
|
||||
},
|
||||
])
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (scrollContainer.value) {
|
||||
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const useAiSuggestion = () => {
|
||||
// 1. 扣费动画
|
||||
creditPill.value?.playDeductAnimation()
|
||||
balance.value -= 5
|
||||
|
||||
// 2. 发送消息
|
||||
messages.value.push({
|
||||
text: '亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。',
|
||||
isMe: true,
|
||||
isAi: true,
|
||||
})
|
||||
|
||||
// 3. 隐藏卡片
|
||||
showAiCard.value = false
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
const sendMsg = () => {
|
||||
if (!inputMsg.value.trim()) return
|
||||
messages.value.push({ text: inputMsg.value, isMe: true, isAi: false })
|
||||
inputMsg.value = ''
|
||||
scrollToBottom()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.4s ease-out;
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-full w-full">
|
||||
<ChatList @select="selectChat" :activeId="activeChatId" />
|
||||
<ChatWindow
|
||||
:chat="activeChat"
|
||||
:credit-balance="creditBalance"
|
||||
@deduct-credit="handleCreditDeduction"
|
||||
@toggle-detail="showDetail = !showDetail"
|
||||
/>
|
||||
<ChatDetail :visible="showDetail" :chat="activeChat" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import ChatList from './ChatList.vue'
|
||||
import ChatWindow from './ChatWindow.vue'
|
||||
import ChatDetail from './ChatDetail.vue'
|
||||
|
||||
const showDetail = ref(false)
|
||||
const activeChatId = ref(1)
|
||||
const creditBalance = ref(12450)
|
||||
|
||||
// 模拟数据
|
||||
const chats = [
|
||||
{
|
||||
id: 1,
|
||||
name: '大野(朋友圈业务)',
|
||||
avatar: 'https://i.pravatar.cc/150?u=8',
|
||||
time: '11:45',
|
||||
msg: '已为您生成 3 条回复建议...',
|
||||
isAi: true,
|
||||
unread: 0,
|
||||
type: 'user',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '京东内购福利群',
|
||||
avatar: 'https://i.pravatar.cc/150?u=group',
|
||||
time: '10:20',
|
||||
msg: '张三: [图片] 谁有这个券?',
|
||||
isAi: false,
|
||||
unread: 99,
|
||||
type: 'group',
|
||||
},
|
||||
]
|
||||
|
||||
const activeChat = computed(() =>
|
||||
chats.find((c) => c.id === activeChatId.value)
|
||||
)
|
||||
|
||||
const selectChat = (id) => {
|
||||
activeChatId.value = id
|
||||
}
|
||||
|
||||
const handleCreditDeduction = (amount) => {
|
||||
creditBalance.value -= amount
|
||||
}
|
||||
</script>
|
||||
@@ -1,306 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-y-auto custom-scroll bg-[#F8FAFC]">
|
||||
<div class="p-8 max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800 mb-1">通讯录管理</h1>
|
||||
<p class="text-sm text-gray-500">管理您的所有联系人</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative">
|
||||
<i
|
||||
class="fa-solid fa-magnifying-glass absolute left-3 top-3 text-gray-400 text-xs"
|
||||
></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索联系人..."
|
||||
class="bg-white border border-gray-200 text-sm pl-9 pr-4 py-2 rounded-lg focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all outline-none w-64"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加联系人
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div
|
||||
class="bg-white rounded-xl p-4 border border-gray-100 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">总联系人</p>
|
||||
<p class="text-2xl font-bold text-slate-800">{{ contacts.length }}</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<i class="fa-solid fa-address-book text-indigo-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white rounded-xl p-4 border border-gray-100 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">最近联系</p>
|
||||
<p class="text-2xl font-bold text-slate-800">
|
||||
{{ recentContactsCount }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<i class="fa-solid fa-clock text-green-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white rounded-xl p-4 border border-gray-100 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-1">未读消息</p>
|
||||
<p class="text-2xl font-bold text-slate-800">{{ unreadCount }}</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<i class="fa-solid fa-envelope text-red-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联系人列表 -->
|
||||
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
<div class="p-4 border-b border-gray-100 flex items-center gap-2">
|
||||
<span
|
||||
v-for="tag in tags"
|
||||
:key="tag.id"
|
||||
@click="selectedTag = tag.id"
|
||||
:class="[
|
||||
'px-3 py-1.5 text-xs font-medium rounded-lg cursor-pointer transition-colors',
|
||||
selectedTag === tag.id
|
||||
? 'bg-indigo-100 text-indigo-700'
|
||||
: 'bg-gray-50 text-gray-600 hover:bg-gray-100',
|
||||
]"
|
||||
>
|
||||
{{ tag.name }}
|
||||
<span
|
||||
v-if="tag.count"
|
||||
class="ml-1 text-xs"
|
||||
:class="selectedTag === tag.id ? 'text-indigo-500' : 'text-gray-400'"
|
||||
>
|
||||
({{ tag.count }})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="contact in filteredContacts"
|
||||
:key="contact.id"
|
||||
class="p-4 border border-gray-100 rounded-xl flex items-center gap-4 hover:shadow-md hover:border-indigo-200 transition-all bg-white group cursor-pointer"
|
||||
@click="openChat(contact)"
|
||||
>
|
||||
<div class="relative">
|
||||
<img
|
||||
:src="contact.avatar"
|
||||
class="w-12 h-12 rounded-full object-cover ring-2 ring-white group-hover:ring-indigo-100 transition-all"
|
||||
:alt="contact.name"
|
||||
/>
|
||||
<div
|
||||
v-if="contact.isOnline"
|
||||
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 bg-green-500 rounded-full border-2 border-white"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="font-semibold text-slate-800 truncate">
|
||||
{{ contact.name }}
|
||||
</div>
|
||||
<span
|
||||
v-if="contact.hasUnread"
|
||||
class="w-2 h-2 bg-red-500 rounded-full flex-shrink-0"
|
||||
></span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 truncate">
|
||||
{{ contact.lastContact || '暂无联系记录' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-0.5">
|
||||
{{ contact.phone || '未绑定手机号' }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click.stop="startChat(contact)"
|
||||
class="opacity-0 group-hover:opacity-100 text-indigo-600 hover:bg-indigo-50 p-2 rounded-lg transition-all"
|
||||
title="发起聊天"
|
||||
>
|
||||
<i class="fa-regular fa-comment-dots"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="filteredContacts.length === 0"
|
||||
class="text-center py-12 text-gray-400"
|
||||
>
|
||||
<i class="fa-solid fa-address-book text-4xl mb-3 opacity-50"></i>
|
||||
<p class="text-sm">暂无联系人</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedTag = ref('all')
|
||||
|
||||
// 标签列表
|
||||
const tags = ref([
|
||||
{ id: 'all', name: '全部', count: 0 },
|
||||
{ id: 'recent', name: '最近联系', count: 0 },
|
||||
{ id: 'unread', name: '未读消息', count: 0 },
|
||||
])
|
||||
|
||||
// 联系人数据
|
||||
const contacts = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '客户 1',
|
||||
avatar: 'https://i.pravatar.cc/150?u=11',
|
||||
phone: '138****8888',
|
||||
lastContact: '最后联系:3天前',
|
||||
isOnline: true,
|
||||
hasUnread: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '客户 2',
|
||||
avatar: 'https://i.pravatar.cc/150?u=12',
|
||||
phone: '139****9999',
|
||||
lastContact: '最后联系:5天前',
|
||||
isOnline: false,
|
||||
hasUnread: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '客户 3',
|
||||
avatar: 'https://i.pravatar.cc/150?u=13',
|
||||
phone: '137****7777',
|
||||
lastContact: '最后联系:1周前',
|
||||
isOnline: true,
|
||||
hasUnread: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '客户 4',
|
||||
avatar: 'https://i.pravatar.cc/150?u=14',
|
||||
phone: '136****6666',
|
||||
lastContact: '最后联系:2周前',
|
||||
isOnline: false,
|
||||
hasUnread: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '客户 5',
|
||||
avatar: 'https://i.pravatar.cc/150?u=15',
|
||||
phone: '135****5555',
|
||||
lastContact: '最后联系:1个月前',
|
||||
isOnline: false,
|
||||
hasUnread: false,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '客户 6',
|
||||
avatar: 'https://i.pravatar.cc/150?u=16',
|
||||
phone: '134****4444',
|
||||
lastContact: '最后联系:今天',
|
||||
isOnline: true,
|
||||
hasUnread: false,
|
||||
},
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const recentContactsCount = computed(() => {
|
||||
return contacts.value.filter((c) => c.lastContact.includes('天')).length
|
||||
})
|
||||
|
||||
const unreadCount = computed(() => {
|
||||
return contacts.value.filter((c) => c.hasUnread).length
|
||||
})
|
||||
|
||||
// 更新标签计数
|
||||
tags.value[0].count = contacts.value.length
|
||||
tags.value[1].count = recentContactsCount.value
|
||||
tags.value[2].count = unreadCount.value
|
||||
|
||||
// 过滤联系人
|
||||
const filteredContacts = computed(() => {
|
||||
let result = contacts.value
|
||||
|
||||
// 按标签过滤
|
||||
if (selectedTag.value === 'recent') {
|
||||
result = result.filter((c) => c.lastContact.includes('天'))
|
||||
} else if (selectedTag.value === 'unread') {
|
||||
result = result.filter((c) => c.hasUnread)
|
||||
}
|
||||
|
||||
// 按搜索关键词过滤
|
||||
if (searchQuery.value.trim()) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(query) ||
|
||||
c.phone?.includes(query) ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 打开聊天
|
||||
const openChat = (contact) => {
|
||||
// TODO: 跳转到聊天页面并打开该联系人
|
||||
console.log('打开聊天:', contact)
|
||||
}
|
||||
|
||||
// 发起聊天
|
||||
const startChat = (contact) => {
|
||||
// TODO: 发起与联系人的聊天
|
||||
console.log('发起聊天:', contact)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条样式 */
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#4F46E5', // Indigo 600
|
||||
primaryLight: '#EEF2FF',
|
||||
accent: '#8B5CF6', // Violet 500
|
||||
accentLight: '#F3E8FF',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Plus Jakarta Sans"', 'Inter', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
51
TouchVueThree/tsconfig.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path Alias */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Types */
|
||||
"types": [
|
||||
"vite/client",
|
||||
"element-plus/global",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
TouchVueThree/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
open: true,
|
||||
port: 5173,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
chunkSizeWarningLimit: 2000,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['vue'],
|
||||
},
|
||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
|
||||
},
|
||||
},
|
||||
minify: 'esbuild',
|
||||
sourcemap: false,
|
||||
},
|
||||
})
|
||||
159
TouchVueThree/vite.config.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// Element Plus 自动导入
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// 性能优化插件
|
||||
import compression from 'vite-plugin-compression'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
|
||||
// 自动导入 Vue 3 API、Vue Router、Pinia、VueUse、TanStack Query
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'pinia',
|
||||
'@vueuse/core',
|
||||
{
|
||||
'@tanstack/vue-query': ['useQuery', 'useMutation', 'useQueryClient', 'useInfiniteQuery']
|
||||
}
|
||||
],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
filepath: './.eslintrc-auto-import.json',
|
||||
globalsPropValue: true,
|
||||
},
|
||||
}),
|
||||
|
||||
// 自动导入 Element Plus 组件
|
||||
Components({
|
||||
resolvers: [
|
||||
ElementPlusResolver({
|
||||
importStyle: 'sass', // 使用 SCSS 样式
|
||||
}),
|
||||
],
|
||||
dts: 'src/components.d.ts',
|
||||
}),
|
||||
|
||||
// Gzip 压缩
|
||||
compression({
|
||||
algorithm: 'gzip',
|
||||
ext: '.gz',
|
||||
threshold: 10240, // 大于 10KB 的文件才压缩
|
||||
}),
|
||||
|
||||
// 打包分析(只在 analyze 模式下启用)
|
||||
mode === 'analyze' && visualizer({
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
filename: 'dist/stats.html'
|
||||
}),
|
||||
].filter(Boolean),
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@api': path.resolve(__dirname, './src/api'),
|
||||
'@components': path.resolve(__dirname, './src/components'),
|
||||
'@composables': path.resolve(__dirname, './src/composables'),
|
||||
'@stores': path.resolve(__dirname, './src/stores'),
|
||||
'@utils': path.resolve(__dirname, './src/utils'),
|
||||
'@types': path.resolve(__dirname, './src/types'),
|
||||
'@views': path.resolve(__dirname, './src/views'),
|
||||
'@assets': path.resolve(__dirname, './src/assets'),
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@use "@/assets/styles/variables.scss" as *;`,
|
||||
api: 'modern-compiler'
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
server: {
|
||||
open: true,
|
||||
port: 8888,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.VITE_API_BASE_URL || 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
sourcemap: false,
|
||||
chunkSizeWarningLimit: 2000,
|
||||
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
// 核心框架
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
|
||||
// UI 组件库
|
||||
'element-plus': ['element-plus', '@element-plus/icons-vue'],
|
||||
|
||||
// 工具库
|
||||
'utils': ['axios', 'dayjs', '@vueuse/core', 'lodash-es', 'mitt', 'nanoid'],
|
||||
|
||||
// 数据管理
|
||||
'query': ['@tanstack/vue-query'],
|
||||
|
||||
// 图表库
|
||||
'echarts': ['echarts', 'vue-echarts'],
|
||||
},
|
||||
|
||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
|
||||
},
|
||||
},
|
||||
|
||||
minify: 'esbuild',
|
||||
target: 'es2015',
|
||||
},
|
||||
|
||||
// 依赖预构建优化
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'pinia',
|
||||
'pinia-plugin-persistedstate',
|
||||
'element-plus',
|
||||
'@element-plus/icons-vue',
|
||||
'@vueuse/core',
|
||||
'axios',
|
||||
'dayjs',
|
||||
'echarts',
|
||||
'lodash-es',
|
||||
'mitt',
|
||||
'nanoid'
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
640
TouchVueThree/静态/index2.html
Normal file
@@ -0,0 +1,640 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<title>触客宝 Pro - 全端自适应版</title>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- FontAwesome -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
<!-- Google Fonts -->
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* 移动端平滑过渡动画 */
|
||||
.slide-enter {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.slide-active {
|
||||
transform: translateX(0);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* PC端自定义细滚动条 */
|
||||
@media (min-width: 768px) {
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配调整 */
|
||||
.mobile-h-screen {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
} /* 解决移动端地址栏遮挡问题 */
|
||||
</style>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#4F46E5', // 品牌蓝
|
||||
primaryLight: '#EEF2FF',
|
||||
},
|
||||
screens: {
|
||||
xs: '480px', // 超小屏幕断点
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body
|
||||
class="bg-gray-100 text-slate-700 overflow-hidden mobile-h-screen flex flex-col md:flex-row"
|
||||
>
|
||||
<!-- 1. PC/平板端左侧边栏 (Desktop Sidebar) -->
|
||||
<!-- 在移动端隐藏 (hidden), 中等屏幕以上显示 (md:flex) -->
|
||||
<aside
|
||||
class="hidden md:flex w-[72px] bg-[#0F172A] flex-col items-center py-6 flex-shrink-0 z-40"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center text-white text-lg shadow-lg mb-8 cursor-pointer hover:scale-110 transition"
|
||||
>
|
||||
<i class="fa-solid fa-robot"></i>
|
||||
</div>
|
||||
|
||||
<nav class="space-y-6 w-full flex flex-col items-center">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center text-white cursor-pointer relative group"
|
||||
>
|
||||
<i class="fa-solid fa-comment-dots"></i>
|
||||
<div
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 h-6 w-1 bg-white rounded-r"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:text-white hover:bg-white/5 cursor-pointer transition"
|
||||
>
|
||||
<i class="fa-solid fa-address-book"></i>
|
||||
</div>
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:text-white hover:bg-white/5 cursor-pointer transition"
|
||||
>
|
||||
<i class="fa-solid fa-chart-pie"></i>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mt-auto flex flex-col items-center gap-4 pb-4">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=wx1"
|
||||
class="w-9 h-9 rounded-lg border-2 border-green-500 cursor-pointer"
|
||||
/>
|
||||
<button class="text-slate-500 hover:text-white">
|
||||
<i class="fa-solid fa-gear text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 2. 中间区域:聊天列表 (Chat List) -->
|
||||
<!-- 移动端:默认显示 (w-full). PC端:固定宽度 (md:w-80) -->
|
||||
<!-- 当移动端进入聊天详情时,这里会被覆盖或隐藏,通过JS控制 'mobile-hidden' 类 -->
|
||||
<div
|
||||
id="chatListPanel"
|
||||
class="w-full md:w-80 bg-white border-r border-gray-200 flex flex-col flex-shrink-0 z-30 transition-transform duration-300"
|
||||
>
|
||||
<!-- 头部 -->
|
||||
<div class="p-4 border-b border-gray-100">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="font-bold text-xl text-slate-800">消息</h2>
|
||||
<!-- 移动端显示的头像 (代替PC侧边栏) -->
|
||||
<div class="md:hidden">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=wx1"
|
||||
class="w-8 h-8 rounded-full border border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 搜索 -->
|
||||
<div class="relative">
|
||||
<i
|
||||
class="fa-solid fa-magnifying-glass absolute left-3 top-3 text-gray-400 text-sm"
|
||||
></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索..."
|
||||
class="w-full bg-slate-50 border-none rounded-xl pl-10 pr-4 py-2.5 text-sm focus:ring-2 focus:ring-primary/20 transition"
|
||||
/>
|
||||
</div>
|
||||
<!-- 标签 -->
|
||||
<div class="flex gap-2 mt-3 overflow-x-auto no-scrollbar">
|
||||
<span
|
||||
class="px-3 py-1 bg-primaryLight text-primary text-xs font-bold rounded-lg whitespace-nowrap"
|
||||
>全部</span
|
||||
>
|
||||
<span
|
||||
class="px-3 py-1 bg-gray-50 text-gray-500 text-xs font-medium rounded-lg whitespace-nowrap"
|
||||
>未读 (12)</span
|
||||
>
|
||||
<span
|
||||
class="px-3 py-1 bg-purple-50 text-purple-600 text-xs font-bold rounded-lg whitespace-nowrap flex items-center gap-1"
|
||||
>
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i> AI托管
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<div class="flex-1 overflow-y-auto custom-scroll pb-20 md:pb-0">
|
||||
<!-- 聊天项 1 -->
|
||||
<div
|
||||
onclick="openChat('user1')"
|
||||
class="p-4 border-b border-gray-50 hover:bg-gray-50 cursor-pointer flex gap-3 items-center active:bg-gray-100 transition"
|
||||
>
|
||||
<div class="relative">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=8"
|
||||
class="w-12 h-12 rounded-xl object-cover"
|
||||
/>
|
||||
<div
|
||||
class="absolute -top-1 -right-1 bg-primary text-white text-[10px] px-1.5 rounded-full border border-white"
|
||||
>
|
||||
AI
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-baseline mb-1">
|
||||
<h3 class="font-bold text-slate-800 text-sm truncate">
|
||||
大野(朋友圈业务)
|
||||
</h3>
|
||||
<span class="text-xs text-gray-400">11:45</span>
|
||||
</div>
|
||||
<p class="text-sm text-primary truncate">
|
||||
已为您生成 3 条回复建议...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 聊天项 2 -->
|
||||
<div
|
||||
onclick="openChat('group1')"
|
||||
class="p-4 border-b border-gray-50 hover:bg-gray-50 cursor-pointer flex gap-3 items-center active:bg-gray-100 transition"
|
||||
>
|
||||
<div class="relative">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=group"
|
||||
class="w-12 h-12 rounded-xl object-cover grayscale opacity-80"
|
||||
/>
|
||||
<div
|
||||
class="absolute -top-1 -right-1 bg-red-500 text-white text-[10px] w-5 h-5 flex items-center justify-center rounded-full border border-white"
|
||||
>
|
||||
99
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex justify-between items-baseline mb-1">
|
||||
<h3 class="font-bold text-slate-700 text-sm truncate">
|
||||
京东内购福利群
|
||||
</h3>
|
||||
<span class="text-xs text-gray-400">10:20</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 truncate">
|
||||
张三: [图片] 谁有这个券?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 主聊天窗口 (Chat Window) -->
|
||||
<!-- 移动端:默认隐藏 (fixed inset-0 translate-x-full). PC端:默认显示 (md:relative md:translate-x-0) -->
|
||||
<main
|
||||
id="chatWindow"
|
||||
class="fixed inset-0 z-50 md:static md:inset-auto bg-[#F8FAFC] flex flex-col flex-1 transform translate-x-full md:translate-x-0 transition-transform duration-300 md:flex"
|
||||
>
|
||||
<!-- 聊天顶部 Header -->
|
||||
<header
|
||||
class="h-14 md:h-16 bg-white/90 backdrop-blur border-b border-gray-200 flex items-center justify-between px-4 sticky top-0 z-10"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 移动端返回按钮 -->
|
||||
<button
|
||||
onclick="closeChat()"
|
||||
class="md:hidden w-8 h-8 -ml-2 flex items-center justify-center text-slate-600 active:bg-gray-100 rounded-full"
|
||||
>
|
||||
<i class="fa-solid fa-chevron-left"></i>
|
||||
</button>
|
||||
<div>
|
||||
<h2
|
||||
class="font-bold text-base md:text-lg text-slate-800 flex items-center gap-2"
|
||||
>
|
||||
大野
|
||||
<span
|
||||
class="text-[10px] px-1.5 py-0.5 bg-green-100 text-green-600 rounded md:inline-block hidden"
|
||||
>在线</span
|
||||
>
|
||||
</h2>
|
||||
<p class="text-xs text-gray-400 md:hidden">微信在线</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 算力胶囊 (Responsive) -->
|
||||
<div
|
||||
class="flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white pl-3 pr-1 py-1 rounded-full shadow-md"
|
||||
>
|
||||
<div class="flex flex-col items-end leading-none">
|
||||
<span class="text-[8px] opacity-80 uppercase tracking-wide"
|
||||
>算力</span
|
||||
>
|
||||
<span class="text-xs font-mono font-bold">12,450</span>
|
||||
</div>
|
||||
<div
|
||||
class="w-6 h-6 bg-white text-indigo-600 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<i class="fa-solid fa-bolt text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 详情开关 -->
|
||||
<button
|
||||
onclick="toggleRightDrawer()"
|
||||
class="w-8 h-8 flex items-center justify-center text-slate-400 hover:text-primary transition"
|
||||
>
|
||||
<i class="fa-solid fa-circle-info text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 聊天内容流 -->
|
||||
<div
|
||||
class="flex-1 overflow-y-auto p-4 md:p-6 space-y-6 custom-scroll bg-[#F0F2F5] md:bg-[#F8FAFC]"
|
||||
id="msgContainer"
|
||||
>
|
||||
<div class="text-center">
|
||||
<span
|
||||
class="text-[10px] text-gray-400 bg-gray-200/50 px-2 py-1 rounded"
|
||||
>今天 10:23</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 对方消息 -->
|
||||
<div class="flex gap-3 items-start">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=8"
|
||||
class="w-9 h-9 rounded-lg shadow-sm"
|
||||
/>
|
||||
<div class="flex flex-col gap-1 max-w-[80%]">
|
||||
<span class="text-[10px] text-gray-400 ml-1">大野</span>
|
||||
<div
|
||||
class="bg-white border border-gray-100 px-4 py-2.5 rounded-2xl rounded-tl-none shadow-sm text-sm text-slate-700 leading-relaxed"
|
||||
>
|
||||
老板,上次说的那个魔兽代练套餐,如果是老客户的话,还能再送一次幻化吗?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 建议卡片 (样式适配) -->
|
||||
<div
|
||||
id="aiCard"
|
||||
class="flex justify-end pr-0 md:pr-12 animate-fade-in-up"
|
||||
>
|
||||
<div
|
||||
class="bg-gradient-to-b from-purple-50 to-white border border-purple-100 rounded-xl p-3 md:p-4 w-full md:w-[400px] shadow-lg relative"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div
|
||||
class="flex items-center gap-2 text-purple-600 text-xs font-bold"
|
||||
>
|
||||
<i class="fa-solid fa-sparkles"></i> AI 建议回复
|
||||
</div>
|
||||
<span
|
||||
class="text-[10px] text-orange-500 bg-orange-50 border border-orange-100 px-1.5 py-0.5 rounded"
|
||||
>-5 算力</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 mb-3 leading-relaxed">
|
||||
“亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。”
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
onclick="hideAiCard()"
|
||||
class="px-3 py-1.5 text-xs text-gray-400 bg-gray-50 rounded-lg"
|
||||
>
|
||||
忽略
|
||||
</button>
|
||||
<button
|
||||
onclick="sendAiMsg()"
|
||||
class="px-4 py-1.5 bg-indigo-600 active:bg-indigo-700 text-white text-xs font-bold rounded-lg shadow-md flex items-center gap-2"
|
||||
>
|
||||
<i class="fa-solid fa-paper-plane"></i> 发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部输入栏 -->
|
||||
<div class="bg-white border-t border-gray-100 p-3 md:p-4 z-20 pb-safe">
|
||||
<!-- 快捷工具行 -->
|
||||
<div class="flex justify-between items-center mb-2 px-1">
|
||||
<div class="flex gap-4 text-gray-400">
|
||||
<i class="fa-regular fa-face-smile text-xl md:text-lg"></i>
|
||||
<i class="fa-regular fa-image text-xl md:text-lg"></i>
|
||||
<i
|
||||
class="fa-solid fa-folder-open text-xl md:text-lg hidden md:block"
|
||||
></i>
|
||||
</div>
|
||||
<!-- 移动端 AI 开关简化 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-bold text-slate-500">AI</span>
|
||||
<div
|
||||
class="w-9 h-5 bg-indigo-600 rounded-full relative"
|
||||
onclick="toggleSwitch(this)"
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 bg-white rounded-full absolute top-0.5 right-0.5 shadow-sm transition-all"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-end">
|
||||
<textarea
|
||||
id="msgInput"
|
||||
class="flex-1 bg-gray-100 md:bg-gray-50 border-none rounded-xl p-3 text-sm focus:ring-1 focus:ring-primary focus:bg-white resize-none h-10 md:h-20 transition-all custom-scroll"
|
||||
placeholder="输入消息..."
|
||||
></textarea>
|
||||
<button
|
||||
onclick="sendUserMsg()"
|
||||
class="bg-slate-900 text-white h-10 w-16 md:w-auto md:px-6 rounded-xl text-sm font-bold flex items-center justify-center shrink-0"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 4. 右侧抽屉 (CRM Drawer) -->
|
||||
<!-- PC端:根据屏幕宽度可常驻或折叠。平板/移动端:模态遮罩弹出 -->
|
||||
<div
|
||||
id="rightDrawerOverlay"
|
||||
onclick="toggleRightDrawer()"
|
||||
class="fixed inset-0 bg-black/20 backdrop-blur-sm z-40 hidden md:hidden"
|
||||
></div>
|
||||
|
||||
<aside
|
||||
id="rightDrawer"
|
||||
class="fixed right-0 top-0 h-full w-[85vw] md:w-80 bg-white shadow-2xl z-50 transform translate-x-full transition-transform duration-300 md:translate-x-full md:relative md:h-auto md:shadow-none md:border-l md:border-gray-200"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<div
|
||||
class="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50"
|
||||
>
|
||||
<h3 class="font-bold text-slate-800">客户资料</h3>
|
||||
<button
|
||||
onclick="toggleRightDrawer()"
|
||||
class="w-8 h-8 bg-white rounded-full shadow-sm text-gray-500 flex items-center justify-center md:hidden"
|
||||
>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-5 custom-scroll space-y-6">
|
||||
<!-- 头像资料 -->
|
||||
<div class="text-center">
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=8"
|
||||
class="w-20 h-20 rounded-full border-4 border-white shadow-lg mx-auto"
|
||||
/>
|
||||
<h3 class="mt-3 font-bold text-lg">大野</h3>
|
||||
<p class="text-xs text-gray-400">ID: ys666wow</p>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<h4 class="text-xs font-bold text-gray-400 uppercase mb-2">标签</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
class="px-2 py-1 bg-red-50 text-red-600 text-xs font-bold rounded border border-red-100"
|
||||
>高意向</span
|
||||
>
|
||||
<span
|
||||
class="px-2 py-1 bg-blue-50 text-blue-600 text-xs font-bold rounded border border-blue-100"
|
||||
>魔兽玩家</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资料卡 -->
|
||||
<div class="bg-gray-50 rounded-xl p-4 space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">备注</span>
|
||||
<span class="font-medium text-slate-700">大野(朋友圈业务)</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">电话</span>
|
||||
<span class="font-medium text-indigo-600">138-0000-8888</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 5. 移动端底部导航 (Mobile Bottom Nav) -->
|
||||
<nav
|
||||
class="md:hidden fixed bottom-0 left-0 w-full bg-white border-t border-gray-200 flex justify-around items-center h-16 z-40 pb-safe shadow-[0_-4px_10px_rgba(0,0,0,0.02)]"
|
||||
id="bottomNav"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 text-primary cursor-pointer w-16"
|
||||
>
|
||||
<i class="fa-solid fa-comment-dots text-xl"></i>
|
||||
<span class="text-[10px] font-medium">消息</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 text-gray-400 cursor-pointer w-16 hover:text-slate-600"
|
||||
>
|
||||
<i class="fa-solid fa-address-book text-xl"></i>
|
||||
<span class="text-[10px] font-medium">通讯录</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 text-gray-400 cursor-pointer w-16 hover:text-slate-600"
|
||||
>
|
||||
<i class="fa-solid fa-chart-pie text-xl"></i>
|
||||
<span class="text-[10px] font-medium">数据</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 text-gray-400 cursor-pointer w-16 hover:text-slate-600"
|
||||
>
|
||||
<img
|
||||
src="https://i.pravatar.cc/150?u=wx1"
|
||||
class="w-6 h-6 rounded-full border border-gray-300"
|
||||
/>
|
||||
<span class="text-[10px] font-medium">我的</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- JS Logic -->
|
||||
<script>
|
||||
// 状态变量
|
||||
let isChatOpen = false
|
||||
let isDrawerOpen = false
|
||||
|
||||
// 1. 打开聊天窗口 (移动端:滑入 / PC:切换内容)
|
||||
function openChat(chatId) {
|
||||
const chatWindow = document.getElementById('chatWindow')
|
||||
const bottomNav = document.getElementById('bottomNav')
|
||||
|
||||
// 移动端逻辑
|
||||
if (window.innerWidth < 768) {
|
||||
chatWindow.classList.remove('translate-x-full')
|
||||
chatWindow.classList.add('translate-x-0')
|
||||
bottomNav.style.display = 'none' // 隐藏底部导航腾出空间
|
||||
isChatOpen = true
|
||||
} else {
|
||||
// PC端逻辑:高亮列表项(这里简化处理)
|
||||
console.log('Switching chat content for ID:', chatId)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 关闭聊天窗口 (仅移动端有效)
|
||||
function closeChat() {
|
||||
const chatWindow = document.getElementById('chatWindow')
|
||||
const bottomNav = document.getElementById('bottomNav')
|
||||
|
||||
chatWindow.classList.remove('translate-x-0')
|
||||
chatWindow.classList.add('translate-x-full')
|
||||
bottomNav.style.display = 'flex'
|
||||
isChatOpen = false
|
||||
}
|
||||
|
||||
// 3. 切换右侧详情抽屉
|
||||
function toggleRightDrawer() {
|
||||
const drawer = document.getElementById('rightDrawer')
|
||||
const overlay = document.getElementById('rightDrawerOverlay')
|
||||
|
||||
if (isDrawerOpen) {
|
||||
// Close
|
||||
drawer.classList.remove('translate-x-0')
|
||||
drawer.classList.add('translate-x-full')
|
||||
if (window.innerWidth >= 768) {
|
||||
// Desktop: Slide out completely or collapse width (depending on preference)
|
||||
// Simple implementation: slide out
|
||||
}
|
||||
overlay.classList.add('hidden')
|
||||
} else {
|
||||
// Open
|
||||
drawer.classList.remove('translate-x-full')
|
||||
drawer.classList.add('translate-x-0')
|
||||
if (window.innerWidth < 768) {
|
||||
overlay.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
isDrawerOpen = !isDrawerOpen
|
||||
}
|
||||
|
||||
// 4. 发送消息逻辑
|
||||
function sendUserMsg() {
|
||||
const input = document.getElementById('msgInput')
|
||||
const container = document.getElementById('msgContainer')
|
||||
const text = input.value.trim()
|
||||
if (!text) return
|
||||
|
||||
appendMsg(text, true, false)
|
||||
input.value = ''
|
||||
|
||||
// 移动端输入后自动恢复高度
|
||||
input.style.height = 'auto'
|
||||
}
|
||||
|
||||
function sendAiMsg() {
|
||||
const aiText =
|
||||
'亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。'
|
||||
appendMsg(aiText, true, true)
|
||||
hideAiCard()
|
||||
}
|
||||
|
||||
function appendMsg(text, isMe, isAi) {
|
||||
const container = document.getElementById('msgContainer')
|
||||
const html = `
|
||||
<div class="flex gap-3 items-end ${isMe ? 'flex-row-reverse' : ''} animate-fade-in-up">
|
||||
<img src="${isMe ? 'https://i.pravatar.cc/150?u=wx1' : 'https://i.pravatar.cc/150?u=8'}" class="w-8 h-8 rounded-lg shadow-sm">
|
||||
<div class="flex flex-col gap-1 max-w-[80%] ${isMe ? 'items-end' : ''}">
|
||||
${isAi ? '<div class="flex items-center gap-1 mb-0.5"><span class="text-[9px] bg-purple-50 text-purple-600 border border-purple-100 px-1 rounded">AI 生成</span></div>' : ''}
|
||||
<div class="p-3 rounded-2xl text-sm leading-relaxed shadow-sm ${isMe ? 'bg-indigo-600 text-white rounded-br-none' : 'bg-white text-slate-700 border border-gray-100 rounded-bl-none'}">
|
||||
${text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
container.insertAdjacentHTML('beforeend', html)
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
|
||||
function hideAiCard() {
|
||||
document.getElementById('aiCard').style.display = 'none'
|
||||
}
|
||||
|
||||
function toggleSwitch(el) {
|
||||
const dot = el.querySelector('div')
|
||||
if (el.classList.contains('bg-indigo-600')) {
|
||||
el.classList.remove('bg-indigo-600')
|
||||
el.classList.add('bg-gray-200')
|
||||
dot.classList.remove('right-0.5')
|
||||
dot.classList.add('left-0.5')
|
||||
} else {
|
||||
el.classList.add('bg-indigo-600')
|
||||
el.classList.remove('bg-gray-200')
|
||||
dot.classList.add('right-0.5')
|
||||
dot.classList.remove('left-0.5')
|
||||
}
|
||||
}
|
||||
|
||||
// Add CSS Animation
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = `
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.animate-fade-in-up { animation: fadeInUp 0.3s ease-out forwards; }
|
||||
.pb-safe { padding-bottom: env(safe-area-inset-bottom); }
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
|
||||
// 监听 PC 端右侧抽屉常驻逻辑 (可选)
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
document.getElementById('bottomNav').style.display = 'none'
|
||||
document.getElementById('rightDrawerOverlay').classList.add('hidden')
|
||||
// PC 端默认不显示遮罩
|
||||
} else {
|
||||
if (!isChatOpen)
|
||||
document.getElementById('bottomNav').style.display = 'flex'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
0
.gitattributes → old/.gitattributes
vendored
0
.gitignore → old/.gitignore
vendored
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
0
package-lock.json → old/package-lock.json
generated
0
pnpm-lock.yaml → old/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |