feat: initial project setup with core infrastructure and api clients
- Add environment configs for dev/staging/prod environments - Implement centralized axios request utility with standardized error handling - Add shared TypeScript types for API responses and requests - Create comprehensive API client modules for all core endpoints - Configure vue router with all application page routes - Add icon fonts, static assets, and loading animations - Set up project documentation and collaboration guidelines - Remove deprecated uni-app bridge component files
This commit is contained in:
11
.env.development
Normal file
11
.env.development
Normal file
@@ -0,0 +1,11 @@
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
# API 基础 URL
|
||||
VITE_API_BASE_URL = 'https://onefeel.brother7.cn/ingress'
|
||||
|
||||
# API 请求超时时间(毫秒)
|
||||
VITE_API_TIMEOUT_MS = 10000
|
||||
|
||||
# Socket 基础 URL
|
||||
VITE_SOCKET_BASE_URL = "wss://onefeel.brother7.cn/ingress/agent/ws/chat"
|
||||
11
.env.production
Normal file
11
.env.production
Normal file
@@ -0,0 +1,11 @@
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
|
||||
# API 基础 URL
|
||||
VITE_API_BASE_URL = 'https://biz.nianxx.cn'
|
||||
|
||||
# API 请求超时时间(毫秒)
|
||||
VITE_API_TIMEOUT_MS = 10000
|
||||
|
||||
# Socket 基础 URL
|
||||
VITE_SOCKET_BASE_URL = "wss://biz.nianxx.cn/ingress/agent/ws/chat"
|
||||
11
.env.staging
Normal file
11
.env.staging
Normal file
@@ -0,0 +1,11 @@
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'staging'
|
||||
|
||||
# API 基础 URL
|
||||
VITE_API_BASE_URL = 'https://onefeel.brother7.cn/ingress'
|
||||
|
||||
# API 请求超时时间(毫秒)
|
||||
VITE_API_TIMEOUT_MS = 10000
|
||||
|
||||
# Socket 基础 URL
|
||||
VITE_SOCKET_BASE_URL = "wss://onefeel.brother7.cn/ingress/agent/ws/chat"
|
||||
205
.trae/documents/统一请求库封装计划.md
Normal file
205
.trae/documents/统一请求库封装计划.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 统一请求库封装计划(src/utils/request.ts 版)
|
||||
|
||||
## Summary
|
||||
|
||||
目标:把统一请求库集中封装在 `src/utils/request.ts`,类型集中在 `src/shared/`,实现:
|
||||
|
||||
- baseURL/timeout/取消请求(Axios Web)
|
||||
- 默认 header 注入:Authorization(Bearer)、clientId、X-Latitude、X-Longitude、language
|
||||
- 业务错误/网络错误/HTTP 错误的统一归一化(只抛错,不做 Toast/跳转)
|
||||
- 输出说明文档到 `docs/request.md`
|
||||
|
||||
## Current State Analysis(以实际仓库为准)
|
||||
|
||||
已确认的事实:
|
||||
|
||||
- 依赖中已包含 `axios`,但 `src/` 内暂无 axios 的实际封装与使用。
|
||||
- `src/utils/request.ts` 存在但为空文件,且命名疑似历史遗留。
|
||||
- 代码中存在大量“缺失模块引用”(详见文末“历史遗留问题清单”),其中 `@/request/api/*` 属于最集中、最影响后续迁移的一类。
|
||||
|
||||
结论:
|
||||
|
||||
- 本次统一请求库规划要做到“新增即可用、可渐进替换”,不依赖现有(缺失的)API 模块结构。
|
||||
|
||||
## Assumptions & Decisions(你已确认)
|
||||
|
||||
- 底层实现:Axios(Web)
|
||||
- baseURL:从 `import.meta.env` 的环境变量读取(本计划固定变量名为 `VITE_API_BASE_URL`)
|
||||
- header 值来源:全局上下文注入(内存态),由登录/定位/语言切换时写入
|
||||
- Authorization:`Bearer <token>`(token 为空则不带 Authorization)
|
||||
- 错误处理:请求库只做错误标准化并抛错,不做 Toast/跳转
|
||||
- header 字段:`Authorization`、`clientId`、`X-Latitude`、`X-Longitude`、`language`
|
||||
|
||||
## 为什么不拆得更细(回答你的问题)
|
||||
|
||||
可以都放在 `src/utils/request.ts` 里完成,且更适合你现在“先落地一个可用的统一入口”的诉求:
|
||||
|
||||
- 单文件落地成本低:少目录、少入口、少导出点,替换/回滚都更直接
|
||||
- 不牺牲可维护性:在单文件里用“内部分区函数 + 类型从 shared 引入”的方式,同样能保持边界清晰
|
||||
- 何时再拆分:当出现“单文件 > 300~500 行、多人同时改动频繁、需要独立单测/Mock”的情况,再把 errors/context/http 拆出去更合理
|
||||
|
||||
因此本计划采用:**实现集中在 request.ts,类型抽到 shared** 的折中方案。
|
||||
|
||||
## Proposed Changes(全新规划)
|
||||
|
||||
### 1) 新增类型目录:`src/shared/`
|
||||
|
||||
新增目录:`src/shared/`
|
||||
|
||||
新增文件(建议):
|
||||
|
||||
- `src/shared/request-types.ts`
|
||||
|
||||
包含的类型定义(稳定、可复用):
|
||||
|
||||
- `export interface ApiResponse<T> { code: number; message?: string; data: T }`
|
||||
- `export interface RequestOptions { signal?: AbortSignal; headers?: Record<string, string>; skipAuth?: boolean }`
|
||||
- `export type RequestContext = { token: string | null; clientId: string | null; latitude: number | null; longitude: number | null; language: string | null }`
|
||||
- `export type NormalizedErrorKind = "business" | "http" | "network" | "unknown"`
|
||||
- `export interface NormalizedError extends Error { kind: NormalizedErrorKind; code?: number; httpStatus?: number; response?: unknown }`
|
||||
|
||||
### 2) 统一请求实现:`src/utils/request.ts`
|
||||
|
||||
新增文件:`src/utils/request.ts`
|
||||
|
||||
职责(全部在一个文件内完成):
|
||||
|
||||
#### 2.1 上下文容器(内存态)
|
||||
|
||||
提供以下导出函数(由业务在合适时机调用):
|
||||
|
||||
- `setAuthToken(token: string | null): void`
|
||||
- `setClientId(clientId: string | null): void`
|
||||
- `setLocation(latitude: number | null, longitude: number | null): void`
|
||||
- `setLanguage(language: string | null): void`
|
||||
- `getRequestContext(): RequestContext`
|
||||
|
||||
默认值策略:
|
||||
|
||||
- `language`:如果未显式设置,尝试读取 `src/i18n/index.ts` 的 `getCurrentLocale()`(读取失败则不注入 language header)
|
||||
|
||||
#### 2.2 Axios 实例创建与默认配置
|
||||
|
||||
- `baseURL`:`import.meta.env.VITE_API_BASE_URL`
|
||||
- 若缺失:直接抛出清晰错误(提示需要配置 baseURL)
|
||||
- `timeout`:可选 `import.meta.env.VITE_API_TIMEOUT_MS`,缺失则默认 15000
|
||||
|
||||
#### 2.3 请求拦截器(header 注入)
|
||||
|
||||
组装 headers 规则:
|
||||
|
||||
- 合并顺序:`默认headers < 上下文headers < 调用方options.headers`
|
||||
- `Authorization`:仅当 token 存在时注入 `Bearer <token>`;当 `options.skipAuth === true` 时跳过
|
||||
- `clientId`:存在则注入
|
||||
- `X-Latitude` / `X-Longitude`:存在则注入(数值转字符串)
|
||||
- `language`:存在则注入
|
||||
|
||||
#### 2.4 响应处理(业务码 + 错误归一化)
|
||||
|
||||
1. 正常响应:
|
||||
- 若响应体符合 `ApiResponse<T>` 且 `code === 0`:返回整个 `ApiResponse<T>`
|
||||
- 若响应体符合 `ApiResponse<T>` 且 `code !== 0`:抛出 `NormalizedError(kind="business", code, message, response)`
|
||||
2. 异常响应(axios error):
|
||||
- 有 `response.status`:抛 `NormalizedError(kind="http", httpStatus, response)`
|
||||
- 无 `response`:抛 `NormalizedError(kind="network")`
|
||||
|
||||
对外导出(建议最小集合):
|
||||
|
||||
- `export async function request<T>(config, options?: RequestOptions): Promise<ApiResponse<T>>`
|
||||
- 可选:`export async function requestData<T>(...): Promise<T>`(内部调用 request 并返回 `data`,用于未来更干净的调用风格)
|
||||
|
||||
### 3) 文档输出到 `docs/`
|
||||
|
||||
新增目录:`docs/`
|
||||
|
||||
新增文档:`docs/request.md`(必须包含):
|
||||
|
||||
- 环境变量约定:`VITE_API_BASE_URL`、`VITE_API_TIMEOUT_MS`(可选)
|
||||
- 上下文写入时机:
|
||||
- 登录成功:`setAuthToken(token)`
|
||||
- 应用初始化:`setClientId(clientId)`
|
||||
- 定位成功:`setLocation(lat, lng)`
|
||||
- 语言切换:`setLanguage(locale)`(或依赖默认读取 i18n)
|
||||
- 使用方式:
|
||||
- 旧代码风格兼容:`const res = await request<T>(); res.data...`
|
||||
- 新代码推荐:`const data = await requestData<T>()`
|
||||
- 错误处理示例:区分 business/http/network/unknown 并给出上层建议策略
|
||||
|
||||
## 历史遗留问题清单(独立梳理,方便逐个替换)
|
||||
|
||||
说明:以下均为“当前仓库扫描可证实”的问题点,按模块引用位置列出,便于你后续按优先级逐个替换/补齐。
|
||||
|
||||
### A. `@/request/api/*` 悬空引用(13 个文件)
|
||||
|
||||
- `src/pages/quick/index.vue`
|
||||
- `src/components/Feedback/index.vue`
|
||||
- `src/components/CreateServiceOrder/index.vue`
|
||||
- `src/pages/service/order/components/OrderCard/index.vue`
|
||||
- `src/pages/booking/components/FooterSection/index.vue`
|
||||
- `src/pages/goods/index.vue`
|
||||
- `src/pages/order/order/components/FooterSection/index.vue`
|
||||
- `src/pages/quick/components/Tabs/index.vue`
|
||||
- `src/pages/order/order/list.vue`
|
||||
- `src/pages/service/order/index.vue`
|
||||
- `src/pages/booking/index.vue`
|
||||
- `src/pages/login/index.vue`
|
||||
- `src/pages/order/order/detail.vue`
|
||||
|
||||
建议后续替换策略:
|
||||
|
||||
1. 先确定“这些 API 模块的真实来源”(是否在其它分支/其它仓库/尚未迁入)。
|
||||
2. 如果确认不会迁回,则逐文件把 `@/request/api/*` 替换为基于 `src/utils/request.ts` 的真实调用(需要你提供具体 URL/参数约定)。
|
||||
|
||||
### B. `@/utils` 目录级导入缺失(6 个文件)
|
||||
|
||||
当前仓库没有 `src/utils/index.*`,但以下文件在导入 `@/utils`:
|
||||
|
||||
- `src/pages/quick/index.vue`
|
||||
- `src/pages/booking/components/FooterSection/index.vue`
|
||||
- `src/pages/goods/index.vue`
|
||||
- `src/pages/order/order/components/FooterSection/index.vue`
|
||||
- `src/pages/booking/index.vue`
|
||||
- `src/pages/home/index.vue`
|
||||
|
||||
### C. `@/store` 与实际目录 `src/stores/` 命名不一致(6 个文件)
|
||||
|
||||
- `src/components/ImageSwiper/index.vue`
|
||||
- `src/pages/quick/components/Card/index.vue`
|
||||
- `src/pages/goods/album/index.vue`
|
||||
- `src/pages/goods/index.vue`
|
||||
- `src/pages/booking/index.vue`
|
||||
- `src/pages/home/index.vue`
|
||||
|
||||
### D. `@/hooks/*` 缺失(3 个文件)
|
||||
|
||||
- `src/components/SwipeCards/index.vue`
|
||||
- `src/pages/login/index.vue`
|
||||
- `src/pages/home/index.vue`
|
||||
|
||||
### E. `@/constant/*` 缺失(6 个文件)
|
||||
|
||||
- `src/components/ModuleTitle/index.vue`
|
||||
- `src/components/Feedback/index.vue`
|
||||
- `src/components/SurveyQuestionnaire/index.vue`
|
||||
- `src/components/CreateServiceOrder/index.vue`
|
||||
- `src/pages/booking/index.vue`
|
||||
- `src/pages/login/index.vue`
|
||||
|
||||
### F. `src/utils/request.ts`(空文件 + 疑似拼写问题)
|
||||
|
||||
- 该文件当前为空,且命名疑似应为 `requests` 或 `request`
|
||||
- 建议后续统一策略:保留/替换/删除前先全仓搜索确认是否在其它分支被引用
|
||||
|
||||
## Verification(执行阶段你手动确认运行)
|
||||
|
||||
你在实现完成后手动执行:
|
||||
|
||||
- `yarn typecheck`
|
||||
- `yarn build`
|
||||
- 如有用例:`yarn test`
|
||||
|
||||
验收标准:
|
||||
|
||||
- `src/utils/request.ts` + `src/shared/request-types.ts` 编译通过
|
||||
- 发起请求时能注入 headers(Authorization/clientId/X-Latitude/X-Longitude/language)
|
||||
- `code !== 0` 时抛出稳定结构的错误对象(上层可根据 kind 做统一处理)
|
||||
77
AGENTS.md
Normal file
77
AGENTS.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 项目协作指南
|
||||
|
||||
本仓库是一个基于 Vite 的 H5 Web 项目。本文档用于约束自动化 Agent 在仓库内的工作方式、目录约定与验证流程。
|
||||
|
||||
## 技术栈与入口
|
||||
|
||||
- 构建:Vite 5(ESM,`package.json` 中 `"type": "module"`)
|
||||
- 框架:Vue 3 + TypeScript
|
||||
- 路由:vue-router 4(入口为 `src/router/index.ts`,当前路由配置较少/可能未完成)
|
||||
- 状态管理:Pinia(`src/stores/`)
|
||||
- UI:Vant 4
|
||||
- 样式:Tailwind CSS v4(通过 `@tailwindcss/vite`),并存在组件级 SCSS
|
||||
- 国际化:vue-i18n(`src/i18n/`,含 Vant 语言同步)
|
||||
- 网络/事件:axios、mitt
|
||||
|
||||
入口文件:
|
||||
|
||||
- 应用入口:`src/main.ts`
|
||||
- 根组件:`src/App.vue`(`<RouterView />`)
|
||||
- 全局样式/主题:`src/styles/main.css`
|
||||
|
||||
## 目录结构与新增代码放置
|
||||
|
||||
- 页面:`src/pages/<module>/...`
|
||||
- 公共组件:`src/components/<ComponentName>/...`
|
||||
- 国际化:`src/i18n/`
|
||||
- 语言包按模块拆分:`src/i18n/modules/{common,home,quick}/`
|
||||
- 工具/请求:`src/utils/`(当前存在 `requets.ts`,文件名可能为历史拼写;不要在未确认引用关系前擅自更名)
|
||||
|
||||
组件目录常见形态(尽量遵循):
|
||||
|
||||
- `src/components/Foo/index.vue`
|
||||
- `src/components/Foo/styles/index.scss`
|
||||
- 可选:`README.md`、`demo.vue`、`prompt.md`、`images/`
|
||||
|
||||
## 代码风格约定
|
||||
|
||||
- Vue 组件优先使用 `<script setup lang="ts">`
|
||||
- 组件样式优先放在同目录 `styles/index.scss`,并在组件内通过 `lang="scss"` + `scoped` 方式引入/编写
|
||||
- 全局主题与 Vant CSS 变量覆盖集中在 `src/styles/main.css`,避免把全局变量散落到页面/组件中
|
||||
- TypeScript:开启 `strict`、`noUnusedLocals`、`noUnusedParameters` 等约束;新增代码需保持零 TS 报错
|
||||
- 路径别名:`@/* -> src/*`(TS 与 Vite 均配置),新增引用优先使用 `@/` 形式
|
||||
|
||||
本仓库未集成 ESLint/Prettier 等统一格式化工具:变更时以“周边现有代码”为准保持一致性。
|
||||
|
||||
## 开发、构建与测试(变更后必须执行)
|
||||
|
||||
使用 Yarn。
|
||||
|
||||
- 安装依赖:`yarn`
|
||||
- 本地开发:`yarn dev`(Vite dev server,端口 5174)
|
||||
- 类型检查:`yarn typecheck`
|
||||
- 构建:`yarn build`(包含 `vue-tsc --noEmit`)
|
||||
- 预览:`yarn preview`
|
||||
- 测试:`yarn test`(Node 原生 test runner,匹配 `src/**/*.test.ts`)
|
||||
|
||||
说明:
|
||||
|
||||
- `tsconfig.json` 排除了 `src/**/*.test.ts`,测试文件如需 TS 类型支持,请在测试内保持自包含,或按项目既有方式处理
|
||||
|
||||
## 修改流程(Agent 工作方式)
|
||||
|
||||
- 先定位:优先通过搜索/阅读定位真实调用链与使用方,再进行修改
|
||||
- 小步提交:单次变更聚焦一个问题点;避免“顺手重构”影响范围
|
||||
- 保守增依赖:不要新增第三方依赖或升级版本;确有必要时先征询确认并说明替代方案
|
||||
- 不破坏接口:组件 props/事件/样式类名若被页面依赖,变更需兼容旧用法或同步修改所有调用点
|
||||
- 变更必验证:完成代码修改后至少运行 `yarn typecheck`;涉及构建与运行差异时再运行 `yarn build` 与相关测试
|
||||
- 开发计算时,有临时分支,需要提醒用户选择分支合并
|
||||
|
||||
## 限制条件
|
||||
|
||||
- 不要废话
|
||||
- 不要缺失代码
|
||||
- 不要出现伪需求
|
||||
- 没有提及的代码不要修改
|
||||
- 需要编译时就提醒用户手动确定
|
||||
- 需要测试时就提醒用户手动执行
|
||||
93
README.md
Normal file
93
README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# nianxx-h5
|
||||
|
||||
基于 Vite 的 H5 Web 项目(Vue 3 + TypeScript)。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vite 5
|
||||
- Vue 3 + TypeScript
|
||||
- vue-router 4
|
||||
- Pinia
|
||||
- Vant 4
|
||||
- Tailwind CSS v4(通过 `@tailwindcss/vite`)
|
||||
- vue-i18n
|
||||
- axios(统一请求库封装见下文)
|
||||
|
||||
## 目录约定
|
||||
|
||||
- 入口:`src/main.ts`
|
||||
- 路由:`src/router/index.ts`
|
||||
- 页面:`src/pages/`
|
||||
- 组件:`src/components/`
|
||||
- 国际化:`src/i18n/`
|
||||
- 全局样式:`src/styles/main.css`
|
||||
- 工具:`src/utils/`
|
||||
- 共享类型:`src/shared/`
|
||||
|
||||
## 本地开发
|
||||
|
||||
使用 Yarn。
|
||||
|
||||
```bash
|
||||
yarn
|
||||
yarn dev
|
||||
```
|
||||
|
||||
常用命令:
|
||||
|
||||
- `yarn dev`:启动开发服务器(端口 5174)
|
||||
- `yarn typecheck`:`vue-tsc --noEmit`
|
||||
- `yarn build`:类型检查 + 构建
|
||||
- `yarn preview`:预览构建产物
|
||||
- `yarn test`:运行 `src/**/*.test.ts`(Node 原生 test runner)
|
||||
|
||||
需要编译/测试时请手动确认执行。
|
||||
|
||||
## 路由
|
||||
|
||||
当前已在 [router/index.ts](file:///d:/www/znkj/nianxx-h5/src/router/index.ts) 配置页面路由(按 `src/pages/` 目录落地):
|
||||
|
||||
- `/`、`/home`:首页
|
||||
- `/login`:登录
|
||||
- `/booking`:预定
|
||||
- `/goods`、`/goods/album`:商品详情/相册
|
||||
- `/quick`、`/quick/list`:快速预定
|
||||
- `/order`、`/order/detail`:订单列表/详情
|
||||
- `/service/order`:工单列表
|
||||
|
||||
## 统一请求库
|
||||
|
||||
统一请求库实现:
|
||||
|
||||
- [request.ts](file:///d:/www/znkj/nianxx-h5/src/utils/request.ts)
|
||||
- 类型定义:[request-types.ts](file:///d:/www/znkj/nianxx-h5/src/shared/request-types.ts)
|
||||
- 使用文档:[docs/request.md](file:///d:/www/znkj/nianxx-h5/docs/request.md)
|
||||
|
||||
必须配置环境变量:
|
||||
|
||||
- `VITE_API_BASE_URL`
|
||||
|
||||
可选配置:
|
||||
|
||||
- `VITE_API_TIMEOUT_MS`(默认 15000)
|
||||
|
||||
## 特殊说明(uni-* 痕迹)
|
||||
|
||||
代码中存在 `uni.*` API、`<uni-icons>` 等用法。若最终运行环境为纯 H5 Web,需要确认这些能力的提供方式(注入/替换/降级)。
|
||||
|
||||
## 历史遗留问题清单(待逐个替换)
|
||||
|
||||
以下为当前仓库可证实的缺失/不一致点,建议按优先级逐步处理:
|
||||
|
||||
- `@/request/api/*` 在多个页面/组件被引用,但仓库内暂无对应实现
|
||||
- `@/utils` 存在目录级导入,但暂无 `src/utils/index.*`
|
||||
- `@/store` 被引用但实际目录为 `src/stores/`
|
||||
- `@/hooks/*`、`@/constant/*` 被引用但当前仓库未找到对应目录
|
||||
- `src/utils/requets.ts` 为空文件且命名疑似拼写遗留
|
||||
|
||||
更详细的引用点列表见:`.trae/documents/统一请求库封装计划.md`。
|
||||
|
||||
## 协作规范
|
||||
|
||||
仓库协作约定见 [AGENTS.md](file:///d:/www/znkj/nianxx-h5/AGENTS.md)。
|
||||
|
||||
131
docs/request.md
Normal file
131
docs/request.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 统一请求库使用说明
|
||||
|
||||
统一请求库实现位置:
|
||||
|
||||
- `src/utils/request.ts`
|
||||
- 类型定义:`src/shared/request-types.ts`
|
||||
|
||||
## 环境变量
|
||||
|
||||
必须配置:
|
||||
|
||||
- `VITE_API_BASE_URL`:接口基础地址(例如 `https://api.example.com`)
|
||||
|
||||
可选配置:
|
||||
|
||||
- `VITE_API_TIMEOUT_MS`:请求超时毫秒数(默认 15000)
|
||||
|
||||
## 上下文注入(header 来源)
|
||||
|
||||
请求库会在请求时自动注入以下 header(有值才注入):
|
||||
|
||||
- `Authorization: Bearer <token>`(可通过 `skipAuth: true` 跳过)
|
||||
- `clientId`
|
||||
- `X-Latitude`
|
||||
- `X-Longitude`
|
||||
- `language`
|
||||
|
||||
你需要在业务合适时机写入上下文(内存态):
|
||||
|
||||
```ts
|
||||
import {
|
||||
setAuthToken,
|
||||
setClientId,
|
||||
setLocation,
|
||||
setLanguage,
|
||||
} from "@/utils/request";
|
||||
|
||||
setClientId("your-client-id");
|
||||
setAuthToken("token-string");
|
||||
setLocation(30.12345, 120.12345);
|
||||
setLanguage("zh-CN");
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `language` 如果未显式设置,会尝试从 i18n 的 `getCurrentLocale()` 获取
|
||||
|
||||
## 发起请求
|
||||
|
||||
### request<T>(兼容旧代码风格,返回 { code, message, data })
|
||||
|
||||
```ts
|
||||
import { request } from "@/utils/request";
|
||||
|
||||
const res = await request<{ records: unknown[] }>({
|
||||
method: "GET",
|
||||
url: "/xxx/list",
|
||||
params: { pageNum: 1, pageSize: 10 },
|
||||
});
|
||||
|
||||
console.log(res.data.records);
|
||||
```
|
||||
|
||||
### requestData<T>(推荐新代码风格,只返回 data)
|
||||
|
||||
```ts
|
||||
import { requestData } from "@/utils/request";
|
||||
|
||||
const data = await requestData<{ records: unknown[] }>({
|
||||
method: "GET",
|
||||
url: "/xxx/list",
|
||||
params: { pageNum: 1, pageSize: 10 },
|
||||
});
|
||||
|
||||
console.log(data.records);
|
||||
```
|
||||
|
||||
### 跳过 Authorization
|
||||
|
||||
```ts
|
||||
import { request } from "@/utils/request";
|
||||
|
||||
await request<string>(
|
||||
{
|
||||
method: "GET",
|
||||
url: "/public/ping",
|
||||
},
|
||||
{ skipAuth: true },
|
||||
);
|
||||
```
|
||||
|
||||
### 临时追加 headers
|
||||
|
||||
```ts
|
||||
import { request } from "@/utils/request";
|
||||
|
||||
await request(
|
||||
{ method: "POST", url: "/xxx", data: { a: 1 } },
|
||||
{ headers: { "X-Debug": "1" } },
|
||||
);
|
||||
```
|
||||
|
||||
## 错误处理(只抛错,不做 Toast)
|
||||
|
||||
请求失败会抛出一个 `RequestError`(符合 `NormalizedError` 结构),可根据 `kind` 分流:
|
||||
|
||||
- `business`:后端返回 `{ code, message, data }` 且 `code !== 0`
|
||||
- `http`:HTTP 状态码错误(如 401/403/500)
|
||||
- `network`:断网/超时等(无 response)
|
||||
- `unknown`:返回结构不符合预期等其它情况
|
||||
|
||||
示例:
|
||||
|
||||
```ts
|
||||
import type { NormalizedError } from "@/shared/request-types";
|
||||
import { requestData } from "@/utils/request";
|
||||
|
||||
try {
|
||||
await requestData({ method: "GET", url: "/xxx" });
|
||||
} catch (e) {
|
||||
const err = e as NormalizedError;
|
||||
if (err.kind === "business") {
|
||||
// err.message / err.code
|
||||
} else if (err.kind === "http") {
|
||||
// err.httpStatus
|
||||
} else if (err.kind === "network") {
|
||||
// err.message
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
46
src/api/goods.ts
Normal file
46
src/api/goods.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { request } from "../utils/request";
|
||||
|
||||
// 获取商品详情
|
||||
export function goodsDetail(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/commodity/commodityDetail",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 订单支付
|
||||
export function orderPay(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/trade/order",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取商品日价格及库存
|
||||
export function commodityDailyPriceList(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/commodity/commodityDailyPriceList",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 分页查询商品分类列表
|
||||
export function commodityTypePageList(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/commodity/commodityTypePageList",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 快速预定分页列表
|
||||
export function quickBookingList(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/quickBookingList",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
90
src/api/home.ts
Normal file
90
src/api/home.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { request } from "../utils/request";
|
||||
|
||||
export function submitFeedback(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/feedback/submitFeedback",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 首页tab场景数据
|
||||
export function homeTabsData(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/queryTagListWithCache",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 首页tab场景下内容数据
|
||||
export function homeTabContentData(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/queryContentListWithCache",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取距离用户最近的标签
|
||||
export function getNearbyTags(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/nearestTag",
|
||||
method: "get",
|
||||
params: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取本地天气
|
||||
export function getLocalWeather(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/getLocalWeather",
|
||||
method: "get",
|
||||
params: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取时间公告列表
|
||||
export function getTimeNoticeList(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/eventList",
|
||||
method: "get",
|
||||
params: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 快速问题列表
|
||||
export function homeQuickQuestionData(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/queryQuickQuestionListWithCache",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 主页数据
|
||||
export function mainPageData(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/mainPageData",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 快速预订组件
|
||||
export function quickBookingComponent(selectedData: any) {
|
||||
const args = { selectedData: selectedData };
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/quickBookingComponent",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 探索发现卡片组件
|
||||
export function discoveryCradComponent() {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/discoveryComponent",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
55
src/api/login.ts
Normal file
55
src/api/login.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { request } from "../utils/request";
|
||||
|
||||
// 获取oauth token
|
||||
export function oauthToken(args: any) {
|
||||
return request({
|
||||
url: "/auth/oauth2/token",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定用户手机号
|
||||
export function bindUserPhone(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/user/bindUserPhone",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 检测用户是否绑定手机号
|
||||
export function checkUserPhone(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/user/checkUserHasBindPhone",
|
||||
method: "get",
|
||||
params: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取登录用户手机号
|
||||
export function getLoginUserPhone(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/user/getLoginUserPhone",
|
||||
method: "get",
|
||||
params: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取服务协议
|
||||
export function getServiceAgreement(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/serviceAgreement",
|
||||
method: "get",
|
||||
params: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取隐私协议
|
||||
export function getPrivacyAgreement(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/mainScene/privacyPolicy",
|
||||
method: "get",
|
||||
params: args,
|
||||
});
|
||||
}
|
||||
55
src/api/order.ts
Normal file
55
src/api/order.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { request } from "../utils/request";
|
||||
|
||||
// 获取用户订单列表
|
||||
export function userOrderList(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/order/userOrderList",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
export function userOrderDetail(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/order/userOrderDetail",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 预下单
|
||||
export function preOrder(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/trade/preOrder",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 订单取消
|
||||
export function orderCancel(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/trade/cancelRefund",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 申请退款
|
||||
export function orderRefund(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/trade/applyRefund",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 未支付订单立即支付
|
||||
export function orderPayNow(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/trade/applyPay",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
10
src/api/upload.ts
Normal file
10
src/api/upload.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { request } from "../utils/request";
|
||||
|
||||
// 上传文件
|
||||
export function uploadFile(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/hotBizCommon/upload",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
36
src/api/workOrder.ts
Normal file
36
src/api/workOrder.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { request } from "../utils/request";
|
||||
|
||||
// 获取用户订单列表
|
||||
export function userWorkOrderList(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/workOrder/userWorkOrderList",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取工单类型
|
||||
export function workOrderTypeListForBiz() {
|
||||
return request({
|
||||
url: "/hotelBiz/workOrder/workOrderTypeListForBiz",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 创建工单
|
||||
export function createWorkOrder(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/workOrder/createWorkOrder",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭工单
|
||||
export function closeWorkOrder(args: any) {
|
||||
return request({
|
||||
url: "/hotelBiz/workOrder/closeWorkOrder",
|
||||
method: "post",
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
17
src/assets/fonts/iconfont.css
Normal file
17
src/assets/fonts/iconfont.css
Normal file
@@ -0,0 +1,17 @@
|
||||
@font-face {
|
||||
font-family: "ZhiNian"; /* Project id 4988933 */
|
||||
src: url("/static/fonts/iconfont.ttf") format("truetype");
|
||||
}
|
||||
|
||||
.ZhiNian {
|
||||
font-family: "ZhiNian" !important;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.zn-food:before {
|
||||
content: "\e607";
|
||||
}
|
||||
|
||||
.zn-clock:before {
|
||||
content: "\e600";
|
||||
}
|
||||
BIN
src/assets/fonts/iconfont.ttf
Normal file
BIN
src/assets/fonts/iconfont.ttf
Normal file
Binary file not shown.
38
src/assets/fonts/znicons.js
Normal file
38
src/assets/fonts/znicons.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export const zniconsMap = {
|
||||
"zn-wifi": "\ue681",
|
||||
"zn-bath": "\ue69a",
|
||||
"zn-frame": "\ue683",
|
||||
"zn-shower-gel": "\ue684",
|
||||
"zn-a-washingmachine": "\ue685",
|
||||
"zn-live": "\ue686",
|
||||
"zn-user": "\ue687",
|
||||
"zn-dish-cover": "\ue688",
|
||||
"zn-glass-cup": "\ue689",
|
||||
"zn-check-circle": "\ue68a",
|
||||
"zn-send-filled": "\ue68b",
|
||||
"zn-nav-arrow-right": "\ue68c",
|
||||
"zn-nav-arrow-left": "\ue68d",
|
||||
"zn-nav-arrow-down": "\ue68e",
|
||||
"zn-fast-arrow-down": "\ue68f",
|
||||
"zn-nav-arrow-up": "\ue690",
|
||||
"zn-microphone": "\ue691",
|
||||
"zn-warning-check-in": "\ue692",
|
||||
"zn-refund": "\ue693",
|
||||
"zn-warning-circle": "\ue694",
|
||||
"zn-dinsh": "\ue6a3",
|
||||
"zn-clock": "\ue6a6",
|
||||
"zn-edit": "\ue6a5",
|
||||
"zn-forkchopsticks": "\ue6a4",
|
||||
"zn-camera": "\ue6a2",
|
||||
"zn-nav-room": "\ue6a1",
|
||||
"zn-nav-ticket": "\ue6a0",
|
||||
"zn-ticket": "\ue69b",
|
||||
"zn-package": "\ue69d",
|
||||
"zn-nav-meal": "\ue69c",
|
||||
"zn-room": "\ue699",
|
||||
"zn-coffee": "\ue698",
|
||||
"zn-refresh": "\ue69f",
|
||||
"zn-keyborad": "\ue697",
|
||||
"zn-hotspring": "\ue696",
|
||||
"zn-bell": "\ue695",
|
||||
};
|
||||
BIN
src/assets/fonts/znicons.ttf
Normal file
BIN
src/assets/fonts/znicons.ttf
Normal file
Binary file not shown.
BIN
src/assets/images/chat_msg_loading.gif
Normal file
BIN
src/assets/images/chat_msg_loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -1,26 +0,0 @@
|
||||
<template></template>
|
||||
<style scoped></style>
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import { saveImageToAlbum } from "@/pages/webdiv/bridge.js";
|
||||
|
||||
onMounted(() => {
|
||||
// 获取页面参数
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.options;
|
||||
|
||||
if (options.imageUrl) {
|
||||
const imageUrl = decodeURIComponent(options.imageUrl);
|
||||
saveImage(imageUrl);
|
||||
}
|
||||
});
|
||||
|
||||
const saveImage = async (imageUrl) => {
|
||||
try {
|
||||
await saveImageToAlbum(imageUrl);
|
||||
} catch (e) {}
|
||||
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template></template>
|
||||
|
||||
<script setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { chooseAndUploadImage } from "@/pages/webdiv/bridge.js";
|
||||
|
||||
onLoad(() => {
|
||||
handleChoose();
|
||||
});
|
||||
|
||||
const sendResult = (imageUrl) => {
|
||||
// 触发全局事件
|
||||
uni.$emit("UPLOAD_RESULT", imageUrl);
|
||||
};
|
||||
const handleChoose = async () => {
|
||||
try {
|
||||
const imageUrl = await chooseAndUploadImage();
|
||||
sendResult(imageUrl);
|
||||
} catch (e) {
|
||||
sendResult("error");
|
||||
}
|
||||
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
@@ -1,288 +0,0 @@
|
||||
# 工单管理系统
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 uniapp + Vue3 组合式 API 开发的微信小程序工单管理系统,提供完整的工单展示、管理和操作功能。
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 页面结构
|
||||
|
||||
```
|
||||
pages/order/
|
||||
├── list.vue # 工单列表主页面
|
||||
├── detail.vue # 工单详情页面
|
||||
├── demo.vue # 功能演示页面
|
||||
├── components/ # 组件目录
|
||||
│ ├── TopNavBar/ # 顶部导航栏组件
|
||||
│ ├── Tabs/ # Tab切换组件
|
||||
│ ├── OrderCard/ # 工单卡片组件
|
||||
│ ├── OrderList/ # 工单列表组件
|
||||
│ └── ConsultationBar/ # 底部咨询栏组件
|
||||
├── styles/ # 样式文件
|
||||
└── images/ # 图片资源
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. TopNavBar - 顶部导航栏组件
|
||||
|
||||
**功能特性:**
|
||||
- 左侧返回按钮
|
||||
- 自适应状态栏高度
|
||||
- 支持自定义标题内容
|
||||
- 响应式设计
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<TopNavBar>
|
||||
<template #title>
|
||||
<Tabs :tabs="tabList" @change="handleTabChange" />
|
||||
</template>
|
||||
</TopNavBar>
|
||||
```
|
||||
|
||||
### 2. Tabs - Tab切换组件
|
||||
|
||||
**功能特性:**
|
||||
- 多标签页切换
|
||||
- 动态下划线指示器
|
||||
- 平滑动画过渡
|
||||
- 自定义标签内容
|
||||
- 固定15px宽度,2px圆角下划线
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<Tabs
|
||||
:tabs="tabList"
|
||||
:defaultActive="0"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. OrderCard - 工单卡片组件
|
||||
|
||||
**功能特性:**
|
||||
- 工单信息展示
|
||||
- 多种状态支持(待处理、处理中、已完成、已取消)
|
||||
- 状态图标和标签
|
||||
- 操作按钮(呼叫、完成)
|
||||
- 自定义操作区域
|
||||
- 响应式设计
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<OrderCard
|
||||
:orderData="orderInfo"
|
||||
@click="handleOrderClick"
|
||||
@call="handleOrderCall"
|
||||
@complete="handleOrderComplete"
|
||||
/>
|
||||
```
|
||||
|
||||
### 4. OrderList - 工单列表组件
|
||||
|
||||
**功能特性:**
|
||||
- 工单列表展示
|
||||
- 下拉刷新
|
||||
- 上拉加载更多
|
||||
- 空状态显示
|
||||
- 加载状态管理
|
||||
- 蓝色渐变背景
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<OrderList
|
||||
:orderList="orderList"
|
||||
:hasMore="hasMore"
|
||||
:isLoading="isLoading"
|
||||
@refresh="handleRefresh"
|
||||
@loadMore="handleLoadMore"
|
||||
/>
|
||||
```
|
||||
|
||||
### 5. ConsultationBar - 底部咨询栏组件
|
||||
|
||||
**功能特性:**
|
||||
- 客服咨询入口
|
||||
- 固定底部显示
|
||||
- 安全区域适配
|
||||
- 自定义咨询文案
|
||||
- 跳转链接支持
|
||||
|
||||
**使用示例:**
|
||||
```vue
|
||||
<ConsultationBar
|
||||
mainText="遇到问题?联系客服"
|
||||
subText="7×24小时在线服务"
|
||||
buttonText="立即咨询"
|
||||
@consultation="handleConsultation"
|
||||
/>
|
||||
```
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 工单数据结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: String, // 工单ID
|
||||
title: String, // 工单标题
|
||||
createTime: String, // 创建时间
|
||||
contactName: String, // 联系人姓名
|
||||
contactPhone: String, // 联系电话
|
||||
status: String, // 工单状态:pending/processing/completed/cancelled
|
||||
type: String // 工单类型:service/order
|
||||
}
|
||||
```
|
||||
|
||||
### Tab数据结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
label: String, // 显示文本
|
||||
value: String // 值
|
||||
}
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### ✅ 已实现功能
|
||||
|
||||
1. **工单管理**
|
||||
- 工单列表展示
|
||||
- 工单状态管理
|
||||
- 工单详情查看
|
||||
- 工单操作(呼叫、完成)
|
||||
|
||||
2. **交互功能**
|
||||
- Tab页面切换
|
||||
- 下拉刷新
|
||||
- 上拉加载更多
|
||||
- 一键拨号
|
||||
- 客服咨询
|
||||
|
||||
3. **UI/UX**
|
||||
- 响应式设计
|
||||
- 暗色模式支持
|
||||
- 流畅动画效果
|
||||
- 优雅的加载状态
|
||||
- 空状态处理
|
||||
|
||||
4. **技术特性**
|
||||
- Vue3 组合式 API
|
||||
- TypeScript 支持
|
||||
- 组件化架构
|
||||
- SCSS 样式管理
|
||||
- 错误处理机制
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 快速开始
|
||||
|
||||
1. **进入工单列表**
|
||||
```javascript
|
||||
uni.navigateTo({
|
||||
url: '/pages/order/list'
|
||||
})
|
||||
```
|
||||
|
||||
2. **查看功能演示**
|
||||
```javascript
|
||||
uni.navigateTo({
|
||||
url: '/pages/order/demo'
|
||||
})
|
||||
```
|
||||
|
||||
### 自定义配置
|
||||
|
||||
1. **修改Tab配置**
|
||||
```javascript
|
||||
const tabList = ref([
|
||||
{ label: "全部订单", value: "all" },
|
||||
{ label: "服务工单", value: "service" },
|
||||
{ label: "自定义Tab", value: "custom" }
|
||||
])
|
||||
```
|
||||
|
||||
2. **自定义工单状态**
|
||||
```javascript
|
||||
const statusMap = {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
custom: '自定义状态'
|
||||
}
|
||||
```
|
||||
|
||||
3. **自定义样式主题**
|
||||
```scss
|
||||
:root {
|
||||
--primary-color: #007AFF;
|
||||
--success-color: #52C41A;
|
||||
--warning-color: #FF8C00;
|
||||
--danger-color: #FF3B30;
|
||||
}
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 工单相关接口
|
||||
|
||||
```javascript
|
||||
// 获取工单列表
|
||||
const getOrderList = async (params) => {
|
||||
return await uni.request({
|
||||
url: '/api/orders',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
|
||||
// 更新工单状态
|
||||
const updateOrderStatus = async (orderId, status) => {
|
||||
return await uni.request({
|
||||
url: `/api/orders/${orderId}/status`,
|
||||
method: 'PUT',
|
||||
data: { status }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **虚拟滚动**:大量数据时建议使用虚拟滚动
|
||||
2. **图片懒加载**:工单图片支持懒加载
|
||||
3. **防抖处理**:搜索和筛选功能防抖优化
|
||||
4. **缓存机制**:Tab切换时数据缓存
|
||||
|
||||
## 兼容性
|
||||
|
||||
- **微信小程序**:✅ 完全支持
|
||||
- **支付宝小程序**:✅ 支持
|
||||
- **H5**:✅ 支持
|
||||
- **App**:✅ 支持
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-15)
|
||||
- 🎉 初始版本发布
|
||||
- ✨ 完整的工单管理功能
|
||||
- ✨ 响应式设计和暗色模式
|
||||
- ✨ 组件化架构
|
||||
- ✨ 完善的文档和演示
|
||||
|
||||
## 开发团队
|
||||
|
||||
- **开发者**:AI Assistant
|
||||
- **技术栈**:uniapp + Vue3 + SCSS
|
||||
- **设计规范**:微信小程序设计指南
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
如有问题或建议,请联系开发团队。
|
||||
@@ -1,181 +0,0 @@
|
||||
# 订单管理系统组件需求文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
订单管理系统的核心组件库,包含工单卡片、工单列表、咨询栏等核心功能组件。
|
||||
|
||||
## TopNavBar 组件
|
||||
|
||||
### 功能要求
|
||||
|
||||
- 顶部导航栏展示
|
||||
- 支持自定义标题内容
|
||||
- 支持插槽扩展
|
||||
|
||||
### 设计要求
|
||||
|
||||
- 固定在页面顶部
|
||||
- 背景色与主题一致
|
||||
- 高度适中,不占用过多空间
|
||||
|
||||
## Tabs 组件
|
||||
|
||||
### 功能要求
|
||||
|
||||
- 标签页切换功能
|
||||
- 支持默认激活项
|
||||
- 切换动画效果
|
||||
- 事件回调
|
||||
|
||||
### 设计要求
|
||||
|
||||
- 标签间距均匀
|
||||
- 激活状态明显
|
||||
- 切换动画流畅
|
||||
- 响应式适配
|
||||
|
||||
## OrderCard 组件
|
||||
|
||||
### 功能要求
|
||||
|
||||
- 展示工单基本信息(标题、时间、联系人、状态等)
|
||||
- 支持点击事件
|
||||
- 支持呼叫功能
|
||||
- 支持完成操作
|
||||
- 状态标识清晰
|
||||
|
||||
### 设计要求
|
||||
|
||||
- 卡片式布局,圆角设计
|
||||
- 信息层次分明
|
||||
- 操作按钮位置合理
|
||||
- 状态颜色区分明显
|
||||
- 支持不同状态的视觉反馈
|
||||
|
||||
## OrderList 组件
|
||||
|
||||
### 功能要求
|
||||
|
||||
- 显示工单列表
|
||||
- 集成z-paging组件,支持虚拟列表
|
||||
- 支持自定义下拉刷新(文案、样式、阈值)
|
||||
- 支持自定义上拉加载更多(文案、样式、阈值)
|
||||
- 自动管理空数据状态
|
||||
- 支持固定高度和自适应高度模式
|
||||
- 完整的事件回调机制
|
||||
- 加载状态管理
|
||||
|
||||
### 设计要求
|
||||
|
||||
- 列表项间距合理
|
||||
- 加载动画流畅
|
||||
- 空状态友好提示
|
||||
- 响应式布局
|
||||
- 虚拟列表优化大数据渲染性能
|
||||
|
||||
### z-paging配置
|
||||
|
||||
- `useVirtualList`: 是否启用虚拟列表(默认true)
|
||||
- `virtualListHeight`: 虚拟列表高度(默认100%)
|
||||
- `cellHeightMode`: 单元格高度模式(auto/fixed)
|
||||
- `fixedHeight`: 固定高度值(当cellHeightMode为fixed时使用)
|
||||
- `customEmptydiv`: 是否使用自定义空状态
|
||||
|
||||
## ConsultationBar 组件
|
||||
|
||||
### 功能要求
|
||||
|
||||
- 底部固定咨询栏
|
||||
- 显示客服信息和联系方式
|
||||
- 支持立即咨询功能
|
||||
- 默认隐藏,点击"立即呼叫"按钮后显示
|
||||
- "立即咨询"按钮单独一行显示
|
||||
- 支持显示/隐藏动画效果
|
||||
|
||||
### 设计要求
|
||||
|
||||
- 固定在页面底部
|
||||
- 背景半透明或纯色
|
||||
- 按钮样式与主题一致
|
||||
- 信息布局清晰
|
||||
- 支持安全区域适配
|
||||
- 显示/隐藏动画流畅
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 工单数据结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: String, // 工单ID
|
||||
title: String, // 工单标题
|
||||
createTime: String, // 创建时间
|
||||
contactName: String, // 联系人姓名
|
||||
contactPhone: String, // 联系电话
|
||||
status: String, // 状态:pending-待处理, processing-处理中, completed-已完成, cancelled-已取消
|
||||
type: String // 类型:service-服务工单, order-普通订单
|
||||
}
|
||||
```
|
||||
|
||||
### Tab数据结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
label: String, // 显示文本
|
||||
value: String // 值
|
||||
}
|
||||
```
|
||||
|
||||
## 技术要求
|
||||
|
||||
### 框架和库
|
||||
|
||||
- Vue 3 Composition API
|
||||
- uni-app框架
|
||||
- z-paging组件(用于列表优化)
|
||||
- SCSS样式预处理
|
||||
|
||||
### 性能优化
|
||||
|
||||
- 虚拟列表支持大数据量渲染
|
||||
- 图片懒加载
|
||||
- 组件按需加载
|
||||
- 合理的缓存策略
|
||||
|
||||
### 兼容性
|
||||
|
||||
- 支持微信小程序
|
||||
- 支持H5
|
||||
- 支持APP
|
||||
- 响应式设计,适配不同屏幕尺寸
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.2.0 (最新)
|
||||
|
||||
- ✅ 集成z-paging组件到OrderList
|
||||
- ✅ 支持虚拟列表,提升大数据渲染性能
|
||||
- ✅ 自定义下拉刷新和上拉加载更多
|
||||
- ✅ 自动管理空数据状态
|
||||
- ✅ 支持固定高度和自适应高度模式
|
||||
- ✅ 完整的事件回调机制
|
||||
- ✅ 创建OrderList演示页面
|
||||
|
||||
### v1.1.0
|
||||
|
||||
- ✅ 修改ConsultationBar组件布局
|
||||
- ✅ "立即咨询"按钮单独一行显示
|
||||
- ✅ 默认隐藏,点击"立即呼叫"后显示
|
||||
- ✅ 添加显示/隐藏动画效果
|
||||
- ✅ 更新相关样式和交互逻辑
|
||||
|
||||
### v1.0.0
|
||||
|
||||
- ✅ 完成OrderCard组件开发
|
||||
- ✅ 完成OrderList组件开发
|
||||
- ✅ 完成ConsultationBar组件开发
|
||||
- ✅ 完成TopNavBar组件开发
|
||||
- ✅ 完成Tabs组件开发
|
||||
- ✅ 完成订单管理页面集成
|
||||
- ✅ 创建组件演示页面
|
||||
- ✅ 编写技术文档
|
||||
@@ -1,23 +1,14 @@
|
||||
<template>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
use-virtual-list
|
||||
:force-close-inner-list="true"
|
||||
cell-height-mode="dynamic"
|
||||
safe-area-inset-bottom
|
||||
@query="queryList"
|
||||
>
|
||||
<z-paging ref="paging" v-model="dataList" use-virtual-list :force-close-inner-list="true" cell-height-mode="dynamic"
|
||||
safe-area-inset-bottom @query="queryList">
|
||||
<template #top>
|
||||
<TopNavBar title="快速预定" :background="$theme - color - 100" />
|
||||
|
||||
<Tabs @change="handleTabChange" />
|
||||
|
||||
<!-- 选择入住、离店日期 0:是酒店 -->
|
||||
<div
|
||||
v-if="didSelectedTabItem && didSelectedTabItem.orderType == 0"
|
||||
class="bg-white border-box flex flex-items-center p-12"
|
||||
>
|
||||
<div v-if="didSelectedTabItem && didSelectedTabItem.orderType == 0"
|
||||
class="bg-white border-box flex flex-items-center p-12">
|
||||
<div class="in flex flex-items-center">
|
||||
<span class="font-size-11 font-500 color-99A0AE mr-4">入住</span>
|
||||
<span class="font-size-14 font-500 color-171717">
|
||||
@@ -26,9 +17,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 几晚 -->
|
||||
<span
|
||||
class="nights bg-E5E8EE border-box font-size-11 font-500 color-525866 rounded-50 ml-8 mr-8"
|
||||
>
|
||||
<span class="nights bg-E5E8EE border-box font-size-11 font-500 color-525866 rounded-50 ml-8 mr-8">
|
||||
{{ selectedDate.totalDays }}晚
|
||||
</span>
|
||||
|
||||
@@ -40,42 +29,26 @@
|
||||
</div>
|
||||
|
||||
<!-- 日期图标 -->
|
||||
<uni-icons
|
||||
class="ml-auto"
|
||||
type="calendar"
|
||||
size="24"
|
||||
color="$theme-color-500"
|
||||
@click="calendarVisible = true"
|
||||
/>
|
||||
<uni-icons class="ml-auto" type="calendar" size="24" color="$theme-color-500" @click="calendarVisible = true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
:item="item"
|
||||
:selectedDate="selectedDate"
|
||||
/>
|
||||
<Card v-for="(item, index) in dataList" :key="index" :item="item" :selectedDate="selectedDate" />
|
||||
</z-paging>
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<Calender
|
||||
:visible="calendarVisible"
|
||||
mode="range"
|
||||
:default-value="selectedDate"
|
||||
@close="handleCalendarClose"
|
||||
@range-select="handleDateSelect"
|
||||
/>
|
||||
<Calender :visible="calendarVisible" mode="range" :default-value="selectedDate" @close="handleCalendarClose"
|
||||
@range-select="handleDateSelect" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import Calender from "@/components/Calender/index.vue";
|
||||
import Tabs from "./components/Tabs/index.vue";
|
||||
import Card from "./components/Card/index.vue";
|
||||
import { quickBookingList } from "@/request/api/GoodsApi";
|
||||
import { DateUtils } from "@/utils";
|
||||
import { quickBookingList } from "@/api/goods";
|
||||
import { DateUtils } from "@/utils/dateUtils";
|
||||
|
||||
const calendarVisible = ref(false);
|
||||
const selectedDate = ref({
|
||||
@@ -1,7 +1,63 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
export const routes = [] satisfies RouteRecordRaw[];
|
||||
export const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: () => import("@/pages/home/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/home",
|
||||
name: "home_alias",
|
||||
component: () => import("@/pages/home/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: () => import("@/pages/login/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/booking",
|
||||
name: "booking",
|
||||
component: () => import("@/pages/booking/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/goods",
|
||||
name: "goods",
|
||||
component: () => import("@/pages/goods/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/goods/album",
|
||||
name: "goods_album",
|
||||
component: () => import("@/pages/goods/album/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/quick",
|
||||
name: "quick",
|
||||
component: () => import("@/pages/quick/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/quick/list",
|
||||
name: "quick_list",
|
||||
component: () => import("@/pages/quick/list.vue"),
|
||||
},
|
||||
{
|
||||
path: "/order",
|
||||
name: "order_list",
|
||||
component: () => import("@/pages/order/order/list.vue"),
|
||||
},
|
||||
{
|
||||
path: "/order/detail",
|
||||
name: "order_detail",
|
||||
component: () => import("@/pages/order/order/detail.vue"),
|
||||
},
|
||||
{
|
||||
path: "/service/order",
|
||||
name: "service_order",
|
||||
component: () => import("@/pages/service/order/index.vue"),
|
||||
},
|
||||
] satisfies RouteRecordRaw[];
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
||||
29
src/shared/request-types.ts
Normal file
29
src/shared/request-types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface ApiResponse<T> {
|
||||
code: number;
|
||||
message?: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
headers?: Record<string, string>;
|
||||
skipAuth?: boolean;
|
||||
}
|
||||
|
||||
export type RequestContext = {
|
||||
token: string | null;
|
||||
clientId: string | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
language: string | null;
|
||||
};
|
||||
|
||||
export type NormalizedErrorKind = "business" | "http" | "network" | "unknown";
|
||||
|
||||
export interface NormalizedError extends Error {
|
||||
kind: NormalizedErrorKind;
|
||||
code?: number;
|
||||
httpStatus?: number;
|
||||
response?: unknown;
|
||||
}
|
||||
|
||||
19
src/utils/dateUtils.ts
Normal file
19
src/utils/dateUtils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 日期工具类
|
||||
* 提供日期格式化功能
|
||||
* */
|
||||
export class DateUtils {
|
||||
/**
|
||||
* 格式化日期为指定格式
|
||||
* @param {Date} date - 日期对象
|
||||
* @param {string} format - 格式化字符串,默认值为 "yyyy-MM-dd"
|
||||
* @returns {string} 格式化后的日期字符串
|
||||
*/
|
||||
static formatDate(date = new Date(), format = "yyyy-MM-dd") {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
|
||||
return format.replace("yyyy", year).replace("MM", month).replace("dd", day);
|
||||
}
|
||||
}
|
||||
210
src/utils/request.ts
Normal file
210
src/utils/request.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import axios from "axios";
|
||||
import type { AxiosError, AxiosRequestConfig } from "axios";
|
||||
import { getCurrentLocale } from "@/i18n";
|
||||
import type {
|
||||
ApiResponse,
|
||||
NormalizedError,
|
||||
NormalizedErrorKind,
|
||||
RequestContext,
|
||||
RequestOptions,
|
||||
} from "@/shared/request-types";
|
||||
|
||||
function resolveBaseURL(): string {
|
||||
const baseURL = (import.meta.env as Record<string, unknown>).VITE_API_BASE_URL;
|
||||
if (typeof baseURL !== "string" || baseURL.trim() === "") {
|
||||
throw new Error("Missing VITE_API_BASE_URL");
|
||||
}
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
function resolveTimeout(): number {
|
||||
const raw = (import.meta.env as Record<string, unknown>).VITE_API_TIMEOUT_MS;
|
||||
if (typeof raw !== "string" || raw.trim() === "") {
|
||||
return 15000;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
return Number.isFinite(parsed) ? parsed : 15000;
|
||||
}
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: resolveBaseURL(),
|
||||
timeout: resolveTimeout(),
|
||||
});
|
||||
|
||||
let context: RequestContext = {
|
||||
token: null,
|
||||
clientId: null,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
language: null,
|
||||
};
|
||||
|
||||
export function setAuthToken(token: string | null): void {
|
||||
context = { ...context, token };
|
||||
}
|
||||
|
||||
export function setClientId(clientId: string | null): void {
|
||||
context = { ...context, clientId };
|
||||
}
|
||||
|
||||
export function setLocation(latitude: number | null, longitude: number | null): void {
|
||||
context = { ...context, latitude, longitude };
|
||||
}
|
||||
|
||||
export function setLanguage(language: string | null): void {
|
||||
context = { ...context, language };
|
||||
}
|
||||
|
||||
export function getRequestContext(): RequestContext {
|
||||
return { ...context };
|
||||
}
|
||||
|
||||
class RequestError extends Error implements NormalizedError {
|
||||
kind: NormalizedErrorKind;
|
||||
code?: number;
|
||||
httpStatus?: number;
|
||||
response?: unknown;
|
||||
|
||||
constructor(params: {
|
||||
message: string;
|
||||
kind: NormalizedErrorKind;
|
||||
code?: number;
|
||||
httpStatus?: number;
|
||||
response?: unknown;
|
||||
}) {
|
||||
super(params.message);
|
||||
this.name = "RequestError";
|
||||
this.kind = params.kind;
|
||||
this.code = params.code;
|
||||
this.httpStatus = params.httpStatus;
|
||||
this.response = params.response;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRequestLanguage(): string | null {
|
||||
if (context.language) {
|
||||
return context.language;
|
||||
}
|
||||
try {
|
||||
return getCurrentLocale();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildContextHeaders(options?: RequestOptions): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (!options?.skipAuth && context.token) {
|
||||
headers.Authorization = `Bearer ${context.token}`;
|
||||
}
|
||||
|
||||
if (context.clientId) {
|
||||
headers.clientId = context.clientId;
|
||||
}
|
||||
|
||||
if (context.latitude !== null && Number.isFinite(context.latitude)) {
|
||||
headers["X-Latitude"] = String(context.latitude);
|
||||
}
|
||||
|
||||
if (context.longitude !== null && Number.isFinite(context.longitude)) {
|
||||
headers["X-Longitude"] = String(context.longitude);
|
||||
}
|
||||
|
||||
const language = resolveRequestLanguage();
|
||||
if (language) {
|
||||
headers.language = language;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function isApiResponse(value: unknown): value is ApiResponse<unknown> {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const v = value as Record<string, unknown>;
|
||||
return typeof v.code === "number" && "data" in v;
|
||||
}
|
||||
|
||||
function normalizeAxiosError(error: AxiosError): RequestError {
|
||||
if (error.response) {
|
||||
const httpStatus = error.response.status;
|
||||
const message = error.message || `HTTP ${httpStatus}`;
|
||||
return new RequestError({
|
||||
kind: "http",
|
||||
httpStatus,
|
||||
message,
|
||||
response: error.response.data,
|
||||
});
|
||||
}
|
||||
|
||||
return new RequestError({
|
||||
kind: "network",
|
||||
message: error.message || "Network Error",
|
||||
});
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
config: AxiosRequestConfig,
|
||||
options: RequestOptions = {},
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const ctxHeaders = buildContextHeaders(options);
|
||||
const mergedHeaders: Record<string, string> = {
|
||||
...ctxHeaders,
|
||||
...((config.headers ?? {}) as Record<string, string>),
|
||||
...(options.headers ?? {}),
|
||||
};
|
||||
|
||||
const res = await http.request<ApiResponse<T>>({
|
||||
...config,
|
||||
signal: options.signal ?? config.signal,
|
||||
headers: mergedHeaders,
|
||||
});
|
||||
|
||||
const body = res.data as unknown;
|
||||
if (!isApiResponse(body)) {
|
||||
throw new RequestError({
|
||||
kind: "unknown",
|
||||
message: "Unexpected response shape",
|
||||
response: body,
|
||||
});
|
||||
}
|
||||
|
||||
if (body.code !== 0) {
|
||||
throw new RequestError({
|
||||
kind: "business",
|
||||
code: body.code,
|
||||
message: body.message || "Business Error",
|
||||
response: body,
|
||||
});
|
||||
}
|
||||
|
||||
return body as ApiResponse<T>;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof RequestError) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (axios.isAxiosError(e)) {
|
||||
throw normalizeAxiosError(e);
|
||||
}
|
||||
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
throw new RequestError({
|
||||
kind: "unknown",
|
||||
message: err.message || "Unknown Error",
|
||||
response: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestData<T>(
|
||||
config: AxiosRequestConfig,
|
||||
options: RequestOptions = {},
|
||||
): Promise<T> {
|
||||
const res = await request<T>(config, options);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user