Merge pull request 'develop' (#2) from develop into master
Reviewed-on: #2
This commit is contained in:
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
| Tab | 路由 | 说明 |
|
| Tab | 路由 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 工作台 | `/home` | 展示关键数据和快捷入口 |
|
| 工作台 | `/home` | 展示快捷入口和近期信息 |
|
||||||
| 订单 | `/orders` | 查询订单、进入详情 |
|
| 订单 | `/orders` | 查询订单、进入详情 |
|
||||||
| 核销 | `/verify` | 手机摄像头扫码核销 |
|
| 核销 | `/verify` | 手机摄像头扫码核销 |
|
||||||
| 事件 | `/events` | 查看事件列表 |
|
| 事件 | `/events` | 查看事件列表 |
|
||||||
@@ -167,10 +167,6 @@
|
|||||||
|
|
||||||
- 当前日期
|
- 当前日期
|
||||||
- 当前账号
|
- 当前账号
|
||||||
- 待使用订单数
|
|
||||||
- 待确认订单数
|
|
||||||
- 生效事件数
|
|
||||||
- 今日订单数
|
|
||||||
- 快捷入口:扫码核销、订单列表、发布事件、事件列表
|
- 快捷入口:扫码核销、订单列表、发布事件、事件列表
|
||||||
- 近期订单
|
- 近期订单
|
||||||
- 近期事件
|
- 近期事件
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -124,6 +124,24 @@ docker run -d \
|
|||||||
docker compose up -d --build
|
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`。
|
`BACKEND_GATEWAY` 是容器内 Nginx 代理到的后端网关地址,不要以 `/` 结尾。生产环境如果前面还有宿主机 Nginx,宿主机 Nginx 只需要负责域名、HTTPS 证书,并把流量转发到 `127.0.0.1:10086`。
|
||||||
|
|
||||||
生产扫码建议通过 HTTPS 访问,否则部分移动浏览器或微信内置浏览器可能无法打开摄像头。
|
生产扫码建议通过 HTTPS 访问,否则部分移动浏览器或微信内置浏览器可能无法打开摄像头。
|
||||||
|
|||||||
13
docker-compose.test.yml
Normal file
13
docker-compose.test.yml
Normal 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
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
- 如果直接把 `VITE_API_BASE_URL` 配成后端网关 IP 或域名,跨域就需要后端网关正确返回 CORS 头;这不是前端代码能完全解决的。
|
- 如果直接把 `VITE_API_BASE_URL` 配成后端网关 IP 或域名,跨域就需要后端网关正确返回 CORS 头;这不是前端代码能完全解决的。
|
||||||
- SPA 路由需要 Nginx `location / { try_files $uri $uri/ /index.html; }`。
|
- SPA 路由需要 Nginx `location / { try_files $uri $uri/ /index.html; }`。
|
||||||
- Docker 部署时,容器内 Nginx 负责静态资源和接口前缀代理,运行时通过 `BACKEND_GATEWAY` 环境变量指定后端网关地址。
|
- Docker 部署时,容器内 Nginx 负责静态资源和接口前缀代理,运行时通过 `BACKEND_GATEWAY` 环境变量指定后端网关地址。
|
||||||
|
- 测试环境使用 `docker-compose.test.yml`,构建参数为 `BUILD_MODE=test`,默认测试网关可通过启动时的 `BACKEND_GATEWAY` 覆盖。
|
||||||
- 如果生产服务器外层还有 Nginx,外层 Nginx 只负责域名、HTTPS 和转发到容器端口;接口前缀代理继续交给容器内 Nginx。
|
- 如果生产服务器外层还有 Nginx,外层 Nginx 只负责域名、HTTPS 和转发到容器端口;接口前缀代理继续交给容器内 Nginx。
|
||||||
|
|
||||||
## 移动端布局
|
## 移动端布局
|
||||||
|
|||||||
@@ -259,40 +259,6 @@ canvas {
|
|||||||
white-space: nowrap;
|
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,
|
.order-card,
|
||||||
.event-card {
|
.event-card {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -461,8 +427,4 @@ canvas {
|
|||||||
.quick-grid {
|
.quick-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { RouterLink, useRouter } from 'vue-router'
|
import { RouterLink, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
@@ -27,10 +27,6 @@ const orders = ref<CommodityOrder[]>([])
|
|||||||
const events = ref<EventData[]>([])
|
const events = ref<EventData[]>([])
|
||||||
const loading = ref(false)
|
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 () => {
|
const loadData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -53,7 +49,7 @@ onMounted(loadData)
|
|||||||
<header class="home-hero">
|
<header class="home-hero">
|
||||||
<div>
|
<div>
|
||||||
<p class="page-kicker">{{ today() }}</p>
|
<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>
|
<p class="page-subtitle">{{ auth.user?.phone || auth.user?.username || '当前员工' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<van-button type="primary" size="small" @click="router.push('/verify')">
|
<van-button type="primary" size="small" @click="router.push('/verify')">
|
||||||
@@ -62,29 +58,6 @@ onMounted(loadData)
|
|||||||
</van-button>
|
</van-button>
|
||||||
</header>
|
</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">
|
<section class="section">
|
||||||
<h2 class="section-title">快捷入口</h2>
|
<h2 class="section-title">快捷入口</h2>
|
||||||
<div class="quick-grid">
|
<div class="quick-grid">
|
||||||
@@ -109,7 +82,7 @@ onMounted(loadData)
|
|||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
待处理订单
|
近期订单
|
||||||
<small @click="router.push('/orders')">全部</small>
|
<small @click="router.push('/orders')">全部</small>
|
||||||
</h2>
|
</h2>
|
||||||
<van-loading v-if="loading" />
|
<van-loading v-if="loading" />
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import StatusTag from '@/components/StatusTag.vue'
|
|||||||
import type { CommodityOrder, CommodityOrderSearch } from '@/types/order'
|
import type { CommodityOrder, CommodityOrderSearch } from '@/types/order'
|
||||||
import { commodityTypeText } from '@/utils/constants'
|
import { commodityTypeText } from '@/utils/constants'
|
||||||
|
|
||||||
const loading = ref(false)
|
const listLoading = ref(false)
|
||||||
|
const requesting = ref(false)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const finished = ref(false)
|
const finished = ref(false)
|
||||||
const records = ref<CommodityOrder[]>([])
|
const records = ref<CommodityOrder[]>([])
|
||||||
@@ -19,25 +20,31 @@ const query = reactive<CommodityOrderSearch>({
|
|||||||
pageSize: 10
|
pageSize: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const buildSearchQuery = (): CommodityOrderSearch => ({
|
||||||
|
...query,
|
||||||
|
contactPhone: keyword.value.trim(),
|
||||||
|
orderStatus: String(status.value || '')
|
||||||
|
})
|
||||||
|
|
||||||
const loadOrders = async (reset = false) => {
|
const loadOrders = async (reset = false) => {
|
||||||
if (loading.value) return
|
if (requesting.value) {
|
||||||
loading.value = true
|
refreshing.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requesting.value = true
|
||||||
|
listLoading.value = true
|
||||||
if (reset) {
|
if (reset) {
|
||||||
query.pageNum = 1
|
query.pageNum = 1
|
||||||
finished.value = false
|
finished.value = false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const searchText = keyword.value.trim()
|
const page = await fetchOrderList(buildSearchQuery())
|
||||||
const page = await fetchOrderList({
|
|
||||||
...query,
|
|
||||||
orderStatus: status.value || undefined,
|
|
||||||
contactPhone: searchText || undefined
|
|
||||||
})
|
|
||||||
records.value = reset ? page.records : [...records.value, ...page.records]
|
records.value = reset ? page.records : [...records.value, ...page.records]
|
||||||
finished.value = records.value.length >= page.total || page.records.length === 0
|
finished.value = records.value.length >= page.total || page.records.length === 0
|
||||||
query.pageNum += 1
|
query.pageNum += 1
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
requesting.value = false
|
||||||
|
listLoading.value = false
|
||||||
refreshing.value = false
|
refreshing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +87,7 @@ onMounted(() => loadOrders(true))
|
|||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
<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">
|
<div v-if="records.length" class="order-list">
|
||||||
<RouterLink v-for="order in records" :key="order.id" class="order-card" :to="`/orders/${order.id}`">
|
<RouterLink v-for="order in records" :key="order.id" class="order-card" :to="`/orders/${order.id}`">
|
||||||
<div class="order-card__main">
|
<div class="order-card__main">
|
||||||
@@ -105,7 +112,7 @@ onMounted(() => loadOrders(true))
|
|||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!loading" class="empty-state">
|
<div v-else-if="!listLoading" class="empty-state">
|
||||||
<ClipboardList :size="30" />
|
<ClipboardList :size="30" />
|
||||||
<div>暂无订单</div>
|
<div>暂无订单</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user