# test-260622-CSS多端适配方案实现
**Repository Path**: zerong-knowledge-sharing/test-260622-CSS_multiTerminalAdaptation
## Basic Information
- **Project Name**: test-260622-CSS多端适配方案实现
- **Description**: PC 端与 H5 端多端适配方案实现
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-06-23
- **Last Updated**: 2026-06-23
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# test-260622-CSS_multiTerminalAdaptation
PC 端与 H5 端多端适配方案实现
# AI Travel Map - 智能旅游地图平台
## 概述
基于 Vue 3 + Vite 构建的"AI+地图"网站平台,支持网页和移动端适配。用户可以通过自然语言与 AI 对话,获取真实的地点推荐、路线规划,并在地图上可视化展示。

## 功能特性
### 核心功能
1. **自然语言交互**
- 用户用口语化表达需求(如:"帮我规划成都 3 天美食游")
- AI 自动解析意图并调用地图能力
- 支持美食、景点、住宿等多种推荐
2. **真实地点推荐与路线规划**
- 基于真实 POI 数据的行程规划
- 多日行程的景点、餐饮串联
- 游玩时长预估和距离计算
3. **地图可视化渲染**
- 路线折线在地图上展示
- 地点标记点(Marker)带颜色区分
- 点击标记查看详情和导航
### 技术栈
| 类别 | 技术方案 |
| ------------ | ----------------------- |
| 框架 | Vue 3 + Composition API |
| 构建工具 | Vite |
| CSS 预处理器 | SCSS |
| 地图服务 | Leaflet + OpenStreetMap |
| 路由 | Vue Router 4 |
| 图标 | SVG (Lucide 风格) |
## 项目结构
```
src/
├── api/
│ └── mockData.js # Mock数据(POI、路线、聊天记录)
├── assets/
│ └── styles/
│ ├── variables.scss # SCSS变量
│ ├── mixins.scss # SCSS混入
│ └── main.scss # 全局样式
├── components/
│ ├── icons.js # 图标渲染函数 + tabs配置
│ ├── MobileHeader.vue # 移动端顶栏
│ ├── DesktopSidebar.vue # 桌面端侧边栏
│ ├── MobileSidebar.vue # 移动端抽屉侧边栏
│ ├── ChatPanel.vue # AI对话面板
│ ├── ChatMessage.vue # 聊天气泡
│ ├── MapView.vue # 地图视图
│ └── RouteTimeline.vue # 行程时间线
├── composables/
│ ├── useResponsive.js # 响应式适配
│ └── useChat.js # 聊天逻辑
├── directives/
│ └── touch.js # 自定义v-touch手势指令
├── App.vue # 主应用(布局编排)
└── main.js # 入口文件
```
## 快速开始
### 1. 安装依赖
```bash
npm install
```
### 2. 启动开发服务器
```bash
npm run dev
```
### 3. 访问应用
打开浏览器访问 `http://localhost:3000`
## 功能演示
### AI 对话示例
在左侧 AI 对话面板中输入:
- `帮我规划成都3天美食游` - 获取完整行程规划
- `推荐好吃的火锅店` - 获取美食推荐
- `推荐景点` - 获取景点推荐
- `推荐住宿` - 获取酒店推荐
### 地图交互
- **查看 POI**: 点击地图上的标记点查看详细信息
- **导航**: 在 POI 详情面板点击"导航前往"
- **路线展示**: AI 生成的行程会以虚线形式在地图上展示
### 响应式适配
- **PC 端** (≥1024px): 左侧边栏 + 右侧地图
- **平板** (768px-1024px): 收缩侧边栏 + 地图
- **移动端** (<768px): 全屏地图 + 底部弹出面板
---
## PC 端与 H5 端适配方案(面试要点)
> 📁 相关源码:`src/composables/useResponsive.js` · `src/App.vue` · `src/components/MobileHeader.vue` · `src/components/DesktopSidebar.vue` · `src/components/MobileSidebar.vue` · `src/assets/styles/mixins.scss`
### 核心思路
PC 和移动端的根本差异在于**屏幕尺寸**和**交互方式**:PC 是宽屏+鼠标精确操作,移动端是窄屏+手指触摸。因此不能简单地等比缩放,而是要**针对不同场景设计不同的布局结构和交互模式**。
### 布局策略
采用**整体布局切换**而非单纯的断点覆盖:
- **PC 端**:左右分栏布局,侧边栏常驻,地图占满剩余空间,信息层级扁平
- **移动端**:全屏地图为主,侧边栏变为抽屉式覆盖层,聊天面板从底部弹出,通过层级和动画管理空间
两者使用完全不同的 DOM 结构(v-if 条件渲染),而不是同一套结构用 CSS 隐藏/显示,这样避免了移动端加载无用 DOM、PC 端多余的遮罩层等问题。
> 📁 参见 `src/App.vue:2-14`(移动端)和 `src/App.vue:21-80`(桌面端)的 v-if 条件渲染
### 断点设计
项目定义了 4 个断点:sm(640px)、md(768px)、lg(1024px)、xl(1280px)。核心分界线是 768px——这是移动端和桌面端的分水岭。断点决策依据:
- 768px 以下:手机竖屏,内容必须单列堆叠
- 768px-1024px:平板,可显示侧边栏但需要收窄
- 1024px 以上:PC 宽屏,完整双栏布局
> 📁 参见 `src/assets/styles/variables.scss:134-138`(断点变量)和 `src/assets/styles/mixins.scss:30-46`(断点 mixin)
### 响应式状态管理
通过 `useResponsive` composable 统一管理设备状态,在 JS 层面响应窗口变化:
- 监听 `window.resize` 事件,实时更新 `width`/`height`
- 派生 `isMobile`/`isTablet`/`isDesktop` 等计算属性
- 组件内通过 `v-if="isMobile"` 切换不同的 UI 结构
这种方案比纯 CSS 方案更灵活——可以在 JS 中根据设备类型执行不同的逻辑(比如移动端点击导航跳转高德 URI,PC 端弹出路线详情)。
> 📁 参见 `src/composables/useResponsive.js:9-11`(isMobile/isTablet/isDesktop 计算属性定义)
> 📁 参见 `src/composables/useResponsive.js:19-21`(onMounted 注册 resize 监听 + 初始调用)
> 📁 参见 `src/composables/useResponsive.js:14-17`(handleResize 回调:同步最新宽高到响应式变量)
> 📁 参见 `src/App.vue:28`(导入 useResponsive)、`src/App.vue:30`(解构 isMobile)
> 📁 参见 `src/App.vue:32`(模板中 `v-if="isMobile"` / `v-if="!isMobile"` 条件渲染切换布局)
### 组件拆分原则
按**设备场景**拆分组件,而非按功能:
- `MobileHeader` / `DesktopSidebar` / `MobileSidebar` — 各自封装对应设备的布局和交互
- `ChatPanel` / `MapView` — 跨设备共享的功能组件
好处是:修改移动端逻辑不会影响 PC 端,两个端可以独立迭代。App.vue 作为布局编排层,只负责根据设备类型组装对应的组件树。
> 📁 参见 `src/App.vue` 模板部分(设备条件渲染组装)
### 触摸适配
移动端额外考虑:
- 按钮最小点击区域 40x40px(手指触控精度要求)
- 侧边栏抽屉加遮罩层,点击遮罩关闭(符合用户心智)
- 聊天面板从底部滑入(符合移动端对话类产品惯例)
- 使用 CSS `touch-action` 优化滚动和手势
- 使用自定义 `v-touch` 指令处理滑动手势
> 📁 参见 `src/directives/touch.js`(自定义 v-touch 指令:支持 swipe/swipeleft/swiperight/swipeup/swipedown/longpress/tap)
> 📁 参见 `src/main.js:4,19`(全局注册 v-touch 指令)
> 📁 参见 `src/components/MobileSidebar.vue:6`(`v-touch.swipeleft` 左滑关闭侧边栏)
> 📁 参见 `src/App.vue:61`(`v-touch.swipedown` 下滑关闭聊天面板)
> 📁 参见 `src/components/ChatPanel.vue`(`.chat-messages { touch-action: pan-y }` 仅允许垂直滚动)
> 📁 参见 `src/components/RouteTimeline.vue`(`.route-days { touch-action: pan-y }` 同上)
> 📁 参见 `src/components/MapView.vue`(`.map { touch-action: none }` 禁用浏览器默认手势,交给 Leaflet 自行处理缩放拖拽)
### 性能考虑
- 条件渲染(v-if)而非 CSS 显隐(v-show),移动端不渲染桌面端 DOM
- 地图组件在两种布局下复用,避免重复初始化
- 移动端减少阴影、模糊等重绘开销较重的效果
### 面试回答模板
> 我们的项目通过**三层机制**实现 PC 和 H5 适配:
>
> 1. **JS 层**:用 composable 监听窗口尺寸,派生设备类型状态,控制条件渲染
> 2. **组件层**:按设备场景拆分独立组件(DesktopSidebar / MobileSidebar / MobileHeader),App.vue 只做编排
> 3. **样式层**:SCSS 断点 + mixins 实现响应式样式,触摸区域适配,布局切换
>
> 关键决策是用 **v-if 条件渲染不同 DOM 结构** 而非 v-show 隐藏,这样移动端不会加载桌面端的无用组件,同时两端可以有完全不同的交互模式(常驻侧边栏 vs 抽屉式覆盖层)。
## Mock 数据说明
项目使用 Mock 数据演示功能:
### POI 数据 (`mockPOIs`)
包含成都地区的景点、餐厅、酒店等真实地点:
- **景点**: 宽窄巷子、武侯祠、锦里、大熊猫基地、杜甫草堂、春熙路
- **美食**: 陈麻婆豆腐、龙抄手、蜀九香火锅、甘食记肥肠粉
- **住宿**: 香格里拉大酒店、太古里博舍酒店、瓦当瓦舍旅行酒店
### 路线数据 (`mockRoutes`)
预设的 3 日行程路线,包含每日景点安排和游玩建议。
## 自定义配置
### 修改地图中心点
编辑 `src/components/MapView.vue` 中的 `center` 属性:
```javascript
const mapCenter = ref([30.5728, 104.0668]); // 成都
```
### 添加新 POI
在 `src/api/mockData.js` 中添加新的 POI 数据:
```javascript
{
id: 'poi_xxx',
name: '地点名称',
type: 'attraction', // attraction | restaurant | hotel | transport | shopping
address: '地址',
lat: 纬度,
lng: 经度,
rating: 4.5,
description: '描述',
tags: ['标签1', '标签2']
}
```
### 添加 AI 响应规则
编辑 `src/composables/useChat.js` 中的 `generateAIResponse` 函数:
```javascript
if (lowerMsg.includes("关键词")) {
return {
content: "AI回复内容",
routeId: "route_001", // 可选:关联路线
};
}
```
## 部署
### 构建生产版本
```bash
npm run build
```
### 预览生产版本
```bash
npm run preview
```
## 组件设计模式(面试要点)
> 📁 相关源码:`src/components/` 目录下所有组件 · `src/composables/` · `src/components/icons.js` · `src/assets/styles/variables.scss` · `src/assets/styles/mixins.scss`
### 组件拆分原则
项目的组件拆分遵循 **「设备场景 × 功能职责」双维度**:
**按设备场景拆分(布局层):**
- `MobileHeader` / `DesktopSidebar` / `MobileSidebar` — 各自封装对应设备的完整 UI 结构
- App.vue 通过 `v-if="isMobile"` 选择渲染哪套组件树
- 好处:修改移动端逻辑不会碰桌面端代码,两端独立迭代
**按功能职责拆分(功能层):**
- `ChatPanel` → 聊天面板(输入+消息列表+空状态)
- `ChatMessage` → 单条消息气泡(纯展示+格式化)
- `RouteTimeline` → 行程时间线(折叠展开+POI 列表)
- `MapView` → 地图容器(Leaflet 初始化+标记+路线)
拆分依据:**一个组件只做一件事**。ChatMessage 不关心消息从哪来,只负责渲染一条消息;MapView 不关心 POI 数据怎么产生的,只负责在地图上画出来。
> 📁 参见 `src/components/ChatMessage.vue:38-43`(只接收一个 message prop)、`src/components/MapView.vue:129-146`(只接收 pois/route/center/zoom)
### 六种组件通信模式
项目中实际用到的通信方式:
**1. Props Down(父 → 子)**
最基础的模式,数据单向流入子组件:
```vue
```
> 📁 参见 `src/App.vue:35-42`(向 ChatPanel 传递 props)、`src/App.vue:78-84`(向 MapView 传递 props)
原则:子组件只读 props,不直接修改。需要变更时 emit 事件。
**2. Events Up(子 → 父)**
子组件通过 emit 向上传递用户操作:
```vue
```
> 📁 参见 `src/App.vue:37-40`(绑定事件监听)、`src/components/ChatPanel.vue:103`(defineEmits 声明)
App.vue 作为事件的收口点,统一处理所有状态变更。
**3. Slots(内容分发)**
DesktopSidebar 使用默认插槽,由父组件决定渲染什么内容:
```vue
```
> 📁 参见 `src/components/DesktopSidebar.vue:32`(`` 插槽定义)、`src/App.vue:33-50`(App.vue 通过插槽注入内容)
好处:DesktopSidebar 不需要知道侧边栏里放什么,只负责外壳(导航栏+折叠逻辑),内容由调用方决定。这就是**容器组件 vs 展示组件**的分离。
**4. Provide / Inject(跨层级传递)**
项目中 `useResponsive` 的 `isMobile` 理论上可以用 provide/inject 全局注入,但当前项目层级不深,直接在 App.vue 用 composable 拿到后通过 props 传下去更清晰。如果组件嵌套超过 3 层,会考虑 provide/inject 避免 props 逐层透传。
**5. Composable 共享状态(兄弟组件通信)**
ChatPanel 和 RouteTimeline 是兄弟组件,它们通过共享的 `useChat` composable 间接通信:
```
App.vue
├── ChatPanel → sendMessage() → messages 变更
└── RouteTimeline → selectedRoute 变更 ← (同一份引用)
```
> 📁 参见 `src/composables/useChat.js:7-9`(模块级 ref 定义,所有调用者共享同一份引用)
不需要事件总线,因为它们读写的是同一份 composable 导出的响应式引用。
**6. 模块级变量(图标/配置共享)**
`icons.js` 导出的图标组件和 tabs 配置,通过 ES Module import 共享:
```javascript
import { tabs, ChatIcon, RouteIcon } from "./icons";
```
> 📁 参见 `src/components/icons.js`(图标渲染函数和 tabs 配置导出)
这是最轻量的共享方式,适合无状态的工具函数和常量。
### 组件设计模式
**容器组件 vs 展示组件**
| 角色 | 组件 | 职责 |
| ---- | -------------- | -------------------------------- |
| 容器 | App.vue | 状态管理、事件处理、数据组装 |
| 容器 | DesktopSidebar | 布局外壳、导航切换 |
| 展示 | ChatPanel | 接收数据、触发事件 |
| 展示 | ChatMessage | 纯渲染、无状态 |
| 展示 | MapView | 接收数据、触发事件 |
| 混合 | RouteTimeline | 内部有折叠状态,但数据来自 props |
> 📁 容器:`src/App.vue`(状态持有+编排)、`src/components/DesktopSidebar.vue`(布局外壳+插槽)
> 📁 展示:`src/components/ChatMessage.vue`(纯渲染)、`src/components/MapView.vue`(只管地图渲染)
> 📁 混合:`src/components/RouteTimeline.vue:110`(expandedDays 局部状态 + props 数据)
**受控组件 vs 非受控组件**
- **受控**:ChatPanel 的 `isCollapsed` 由内部管理(非受控),但 `messages` 由父组件传入(受控数据)
- **受控**:DesktopSidebar 的 `collapsed` 由父组件 App.vue 控制(完全受控)
- 判断标准:**状态是否需要被父组件感知或影响其他组件**。需要就受控,不需要就非受控。
> 📁 参见 `src/components/DesktopSidebar.vue:36-37`(collapsed 受控 prop)、`src/components/ChatPanel.vue:108`(isCollapsed 非受控内部状态)
### 设计系统复用
所有组件共享同一套设计语言:
- **SCSS Variables** — 颜色、字体、间距、圆角、阴影等 token 统一定义
- **SCSS Mixins** — `flex-center`、`button-primary`、`card` 等可复用样式模式
- **BEM 命名** — `chat-message__bubble`、`route-day__header`,语义化且避免样式冲突
- **Scoped Styles** — 每个组件样式隔离,不污染全局
新增组件时只需要 `@import variables.scss + mixins.scss`,就能用全套设计 token,保证视觉一致性。
> 📁 参见 `src/assets/styles/variables.scss`(51 个颜色变量 + 间距/字号/圆角/阴影/断点/过渡等 token)
> 📁 参见 `src/assets/styles/mixins.scss`(flex/position/button/card/animation 等可复用 mixin)
> 📁 参见 `src/components/ChatPanel.vue:160-162`(组件引入设计系统的标准写法)
### 第三方库集成策略
以 Leaflet 为例,项目采用 **动态导入 + 组件封装**:
- `MapView.vue` 内部 `import('leaflet')` 动态加载,不在主 bundle 中
- Leaflet 的初始化、销毁、标记管理全部封装在 MapView 内部
- 父组件只通过 props 传入 `pois`、`route`、`center`,不接触 Leaflet API
好处:第三方库的复杂度被隔离在单个组件内,替换地图 SDK 只需改 MapView 一个文件。
> 📁 参见 `src/components/MapView.vue:161`(`import('leaflet').then()` 动态导入)、`src/components/MapView.vue:299-307`(onMounted 初始化 + onUnmounted 销毁)
### 面试回答模板
> 我们的组件设计遵循三个原则:
>
> 1. **单一职责**:每个组件只做一件事。ChatMessage 只渲染气泡,MapView 只管地图,App.vue 只做编排
> 2. **明确的通信边界**:Props Down + Events Up 为主,Slots 实现内容分发,Composable 解决兄弟组件通信。不搞事件总线、不搞全局 bus
> 3. **设备场景拆分**:按 PC/移动端拆出独立的布局组件(DesktopSidebar / MobileSidebar),而不是一套 DOM 用 CSS 切来切去
>
> 设计系统通过 SCSS Variables + Mixins + BEM 命名统一维护,新组件接入零成本。第三方库(如 Leaflet)用动态导入 + 组件封装隔离复杂度。
### 为什么没用 Vuex / Pinia
项目没有引入 Vuex 或 Pinia,而是采用 **Composable + provide/inject** 的轻量方案。原因:
1. **状态量级不大**:核心状态就是聊天消息列表、选中的路线、加载状态、设备类型,没有复杂的模块间状态依赖
2. **状态作用域清晰**:聊天状态只在 ChatPanel ↔ App 之间流转,设备状态全局共享但只读,不存在跨模块状态竞争
3. **避免过度设计**:对于中小型项目,引入状态管理库会增加心智负担和样板代码,composable 已经够用
> 📁 参见 `src/composables/useChat.js`(完整聊天状态管理)、`src/composables/useResponsive.js`(设备状态管理)
面试时可以补充:如果项目规模增长(比如加入用户登录、收藏夹、多行程管理、离线缓存),会考虑引入 Pinia,因为它的 TypeScript 支持更好、devtools 集成更完善。
### 状态分层架构
项目的状态分为三层:
**第一层:全局共享状态(Composable 单例)**
- `useChat` — 聊天消息、AI 加载状态、选中的路线。通过 composable 封装,多次调用返回同一个响应式引用(模块级变量),等价于一个轻量 store
- `useResponsive` — 设备类型(isMobile/isTablet/isDesktop)。监听 resize 事件,全局只读
> 📁 参见 `src/composables/useChat.js:7-9`(模块级 ref 定义)、`src/composables/useResponsive.js:6-11`(设备状态计算属性)
**第二层:组件局部状态(ref)**
- `showSidebar`、`showChat`、`activeTab` 等 UI 状态,只属于 App.vue
- `inputText`、`isCollapsed` 等只属于具体组件
- 这些状态不需要共享,直接用 `ref()` 管理
> 📁 参见 `src/App.vue:56-60`(App.vue 局部 UI 状态)、`src/components/ChatPanel.vue:105-108`(ChatPanel 局部状态)
**第三层:静态数据(模块导出)**
- `mockPOIs`、`mockRoutes`、`mockChatHistory` 等 Mock 数据
- 作为 ES Module 常量导入,不参与响应式系统
- 如果后续对接真实 API,这一层会变成异步请求 + 缓存
> 📁 参见 `src/api/mockData.js`(Mock 数据定义)
### Composable 作为 Store 的实现原理
`useChat` 的核心设计:
```javascript
// 模块级变量 — 所有调用者共享同一份引用
const messages = ref([...mockChatHistory]);
const selectedRoute = ref(null);
export function useChat() {
return { messages, selectedRoute, sendMessage };
}
```
- `ref` 定义在函数**外部**(模块作用域),而不是函数内部
- 多个组件调用 `useChat()` 拿到的是**同一个** `messages` 引用
- 本质上就是一个没有 devtools 的简易 Pinia store
> 📁 参见 `src/composables/useChat.js:7-9`(模块级 ref)、`src/composables/useChat.js:98-105`(return 导出)
### 数据流向
采用标准的 **Props Down, Events Up** 单向数据流:
```
App.vue (状态持有者)
├── DesktopSidebar ← :activeTab, :collapsed
│ └── ChatPanel ← :messages, :isLoading
│ └── ChatMessage ← :message
├── MobileSidebar ← :visible, :activeTab
├── MapView ← :pois, :route, :center, :zoom
└── MobileHeader (无状态,纯展示)
```
- **App.vue** 是状态的「单一数据源」,持有所有共享状态
- **子组件**通过 props 接收数据,通过 emit 向上传递事件
- **没有子组件直接修改父组件状态**,所有状态变更都在 App.vue 的 handler 中执行
> 📁 参见 `src/App.vue:28-100`(模板中所有 props 绑定和事件监听)
### 状态变更流程
以「用户发送聊天消息」为例:
1. ChatPanel emit `send` 事件 → App.vue `handleSendMessage`
2. App.vue 调用 `useChat().sendMessage()`
3. `sendMessage` 内部:push 用户消息 → 设置 loading → 模拟延迟 → push AI 回复 → 取消 loading
4. `messages` 是共享引用,ChatPanel 通过 watch 自动响应新消息
整个流程是**单向的**:事件向上 → 状态在顶层变更 → 响应式自动向下传播。
> 📁 参见 `src/App.vue:62-65`(handleSendMessage)、`src/composables/useChat.js:58-91`(sendMessage 实现)、`src/components/ChatPanel.vue:156-157`(watch messages 变更自动滚动)
### 如果用 Pinia 会怎样
对于当前项目规模,Pinia 的优势体现不出来。但如果项目扩展到以下场景,引入 Pinia 是值得的:
| 场景 | 当前方案 | Pinia 方案 |
| ------------------- | ---------------------- | -------------------------------------- |
| 聊天历史持久化 | 需要手动 localStorage | `pinia-plugin-persistedstate` 一行搞定 |
| 多页面共享状态 | composable 模块变量 | Store 天然全局 |
| DevTools 调试 | 无法追踪状态变更时间线 | Pinia devtools 完整支持 |
| TypeScript 类型推导 | 需要手动定义返回类型 | `defineStore` 自动推导 |
| 状态模块化 | composable 按功能拆分 | Store 按模块拆分,支持嵌套 |
### 面试回答模板
> 我们的项目采用 **Composable 作为轻量 Store** 的方案,没有引入 Vuex/Pinia。
>
> 核心思路是:将共享状态定义在 composable 函数的**模块作用域**(函数外部),多次调用返回同一个引用,实现跨组件共享。本质上和 Pinia 的原理一样——都是利用 ES Module 的单例特性。
>
> 数据流遵循 **Props Down, Events Up** 的单向流:App.vue 作为状态的单一数据源,子组件通过 props 接收、emit 事件触发变更。
>
> 选择这个方案的原因是项目状态量级不大、作用域清晰,引入 Pinia 会增加不必要的复杂度。但如果项目增长到需要持久化、DevTools 调试、复杂模块间状态依赖,会考虑迁移到 Pinia。
- [ ] 接入真实后端 API
- [ ] 集成高德/百度地图 API
- [ ] 添加用户登录和收藏功能
- [ ] 支持多语言
- [ ] 添加语音交互
- [ ] 集成天气信息
- [ ] 添加导航功能
## 参考资料
- [Vue 3 官方文档](https://vuejs.org/)
- [Vite 官方文档](https://vitejs.dev/)
- [Leaflet 官方文档](https://leafletjs.com/)
- [SCSS 官方文档](https://sass-lang.com/)