Merge pull request 'feature/huangting' (#1) from feature/huangting into develop

Reviewed-on: #1
This commit is contained in:
2026-06-30 11:54:43 +08:00
7 changed files with 55 additions and 85 deletions

View File

@@ -50,7 +50,7 @@
| Tab | 路由 | 说明 |
|---|---|---|
| 工作台 | `/home` | 展示关键数据和快捷入口 |
| 工作台 | `/home` | 展示快捷入口和近期信息 |
| 订单 | `/orders` | 查询订单、进入详情 |
| 核销 | `/verify` | 手机摄像头扫码核销 |
| 事件 | `/events` | 查看事件列表 |
@@ -167,10 +167,6 @@
- 当前日期
- 当前账号
- 待使用订单数
- 待确认订单数
- 生效事件数
- 今日订单数
- 快捷入口:扫码核销、订单列表、发布事件、事件列表
- 近期订单
- 近期事件

View File

@@ -124,6 +124,24 @@ docker run -d \
docker compose up -d --build
```
测试环境使用独立 compose 文件:
```bash
docker compose -f docker-compose.test.yml up -d --build
```
测试环境默认后端网关是:
```bash
BACKEND_GATEWAY=http://8.138.234.141/ingress
```
如果测试服务器上的网关地址不同,可以启动时覆盖:
```bash
BACKEND_GATEWAY=http://测试网关地址 H5_PORT=10086 docker compose -f docker-compose.test.yml up -d --build
```
`BACKEND_GATEWAY` 是容器内 Nginx 代理到的后端网关地址,不要以 `/` 结尾。生产环境如果前面还有宿主机 Nginx宿主机 Nginx 只需要负责域名、HTTPS 证书,并把流量转发到 `127.0.0.1:10086`
生产扫码建议通过 HTTPS 访问,否则部分移动浏览器或微信内置浏览器可能无法打开摄像头。

13
docker-compose.test.yml Normal file
View File

@@ -0,0 +1,13 @@
services:
hotel-biz-h5-test:
build:
context: .
args:
BUILD_MODE: test
image: hotel-biz-h5:test
container_name: hotel-biz-h5-test
environment:
BACKEND_GATEWAY: ${BACKEND_GATEWAY:-http://8.138.234.141/ingress}
ports:
- "${H5_PORT:-10086}:10086"
restart: unless-stopped

View File

@@ -34,6 +34,7 @@
- 如果直接把 `VITE_API_BASE_URL` 配成后端网关 IP 或域名,跨域就需要后端网关正确返回 CORS 头;这不是前端代码能完全解决的。
- SPA 路由需要 Nginx `location / { try_files $uri $uri/ /index.html; }`
- Docker 部署时,容器内 Nginx 负责静态资源和接口前缀代理,运行时通过 `BACKEND_GATEWAY` 环境变量指定后端网关地址。
- 测试环境使用 `docker-compose.test.yml`,构建参数为 `BUILD_MODE=test`,默认测试网关可通过启动时的 `BACKEND_GATEWAY` 覆盖。
- 如果生产服务器外层还有 Nginx外层 Nginx 只负责域名、HTTPS 和转发到容器端口;接口前缀代理继续交给容器内 Nginx。
## 移动端布局

View File

@@ -259,40 +259,6 @@ canvas {
white-space: nowrap;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.stat-card {
min-height: 92px;
padding: 14px;
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow);
}
.stat-label {
color: var(--text-muted);
font-size: 12px;
line-height: 1.4;
}
.stat-value {
margin-top: 8px;
color: var(--text-strong);
font-size: 25px;
font-weight: 800;
line-height: 1;
}
.stat-hint {
margin-top: 8px;
color: var(--primary-deep);
font-size: 12px;
}
.order-card,
.event-card {
display: block;
@@ -461,8 +427,4 @@ canvas {
.quick-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stat-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import {
CalendarDays,
@@ -27,10 +27,6 @@ const orders = ref<CommodityOrder[]>([])
const events = ref<EventData[]>([])
const loading = ref(false)
const todoOrders = computed(() => orders.value.filter((item) => item.orderStatus === '2'))
const confirmOrders = computed(() => orders.value.filter((item) => item.orderStatus === '1'))
const activeEvents = computed(() => events.value.filter((item) => item.showStatusDesc === '生效中'))
const loadData = async () => {
loading.value = true
try {
@@ -53,7 +49,7 @@ onMounted(loadData)
<header class="home-hero">
<div>
<p class="page-kicker">{{ today() }}</p>
<h1 class="page-title">员工工作台</h1>
<h1 class="page-title">工作台</h1>
<p class="page-subtitle">{{ auth.user?.phone || auth.user?.username || '当前员工' }}</p>
</div>
<van-button type="primary" size="small" @click="router.push('/verify')">
@@ -62,29 +58,6 @@ onMounted(loadData)
</van-button>
</header>
<section class="stat-grid section">
<div class="stat-card">
<div class="stat-label">待使用订单</div>
<div class="stat-value">{{ todoOrders.length }}</div>
<div class="stat-hint">可进入详情核销</div>
</div>
<div class="stat-card">
<div class="stat-label">待确认订单</div>
<div class="stat-value">{{ confirmOrders.length }}</div>
<div class="stat-hint">需要员工处理</div>
</div>
<div class="stat-card">
<div class="stat-label">生效事件</div>
<div class="stat-value">{{ activeEvents.length }}</div>
<div class="stat-hint">展示给员工</div>
</div>
<div class="stat-card">
<div class="stat-label">今日订单</div>
<div class="stat-value">{{ orders.length }}</div>
<div class="stat-hint">默认近期待办</div>
</div>
</section>
<section class="section">
<h2 class="section-title">快捷入口</h2>
<div class="quick-grid">
@@ -109,7 +82,7 @@ onMounted(loadData)
<section class="section">
<h2 class="section-title">
待处理订单
近期订单
<small @click="router.push('/orders')">全部</small>
</h2>
<van-loading v-if="loading" />

View File

@@ -7,7 +7,8 @@ import StatusTag from '@/components/StatusTag.vue'
import type { CommodityOrder, CommodityOrderSearch } from '@/types/order'
import { commodityTypeText } from '@/utils/constants'
const loading = ref(false)
const listLoading = ref(false)
const requesting = ref(false)
const refreshing = ref(false)
const finished = ref(false)
const records = ref<CommodityOrder[]>([])
@@ -19,25 +20,31 @@ const query = reactive<CommodityOrderSearch>({
pageSize: 10
})
const buildSearchQuery = (): CommodityOrderSearch => ({
...query,
contactPhone: keyword.value.trim(),
orderStatus: String(status.value || '')
})
const loadOrders = async (reset = false) => {
if (loading.value) return
loading.value = true
if (requesting.value) {
refreshing.value = false
return
}
requesting.value = true
listLoading.value = true
if (reset) {
query.pageNum = 1
finished.value = false
}
try {
const searchText = keyword.value.trim()
const page = await fetchOrderList({
...query,
orderStatus: status.value || undefined,
contactPhone: searchText || undefined
})
const page = await fetchOrderList(buildSearchQuery())
records.value = reset ? page.records : [...records.value, ...page.records]
finished.value = records.value.length >= page.total || page.records.length === 0
query.pageNum += 1
} finally {
loading.value = false
requesting.value = false
listLoading.value = false
refreshing.value = false
}
}
@@ -80,7 +87,7 @@ onMounted(() => loadOrders(true))
</van-tabs>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多订单" @load="loadOrders">
<van-list v-model:loading="listLoading" :finished="finished" finished-text="没有更多订单" @load="loadOrders">
<div v-if="records.length" class="order-list">
<RouterLink v-for="order in records" :key="order.id" class="order-card" :to="`/orders/${order.id}`">
<div class="order-card__main">
@@ -105,7 +112,7 @@ onMounted(() => loadOrders(true))
</div>
</RouterLink>
</div>
<div v-else-if="!loading" class="empty-state">
<div v-else-if="!listLoading" class="empty-state">
<ClipboardList :size="30" />
<div>暂无订单</div>
</div>