优化测试部署和页面展示细节
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
VITE_USE_MOCK=false
|
||||
VITE_APP_BASE=/
|
||||
VITE_PROXY_TARGET=
|
||||
# VITE_API_BASE_URL=http://8.138.234.141/ingress
|
||||
VITE_API_BASE_URL=
|
||||
|
||||
@@ -20,6 +20,21 @@ server {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ^~ /hotel-staff-h5/assets/ {
|
||||
rewrite ^/hotel-staff-h5/(.*)$ /$1 break;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /hotel-staff-h5 {
|
||||
return 301 /hotel-staff-h5/;
|
||||
}
|
||||
|
||||
location ^~ /hotel-staff-h5/ {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
proxy_pass ${BACKEND_GATEWAY}/auth/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -8,6 +8,11 @@ const http = axios.create({
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
export type RequestError = Error & {
|
||||
cause?: unknown
|
||||
toastShown?: boolean
|
||||
}
|
||||
|
||||
let isRedirectingToLogin = false
|
||||
|
||||
const authExpiredTexts = ['请求令牌已过期', '令牌已过期', 'token已过期', 'Token已过期', 'invalid_token']
|
||||
@@ -20,6 +25,13 @@ const getHeader = (headers: unknown, key: string) => {
|
||||
|
||||
const hasBearerAuthorization = (headers: unknown) => getHeader(headers, 'Authorization').startsWith('Bearer ')
|
||||
|
||||
const createRequestError = (message: string, cause?: unknown, toastShown = false) => {
|
||||
const requestError = new Error(message) as RequestError
|
||||
requestError.cause = cause
|
||||
requestError.toastShown = toastShown
|
||||
return requestError
|
||||
}
|
||||
|
||||
const isAuthExpired = (status?: number, code?: unknown, message = '', hasBearerToken = false) => {
|
||||
const normalizedCode = String(code || '')
|
||||
return (
|
||||
@@ -32,7 +44,7 @@ const redirectToLogin = (message: string) => {
|
||||
clearAuthStorage()
|
||||
window.dispatchEvent(new Event(AUTH_EXPIRED_EVENT))
|
||||
|
||||
if (isRedirectingToLogin || window.location.pathname.endsWith('/login')) return
|
||||
if (isRedirectingToLogin || window.location.pathname.endsWith('/login')) return false
|
||||
isRedirectingToLogin = true
|
||||
showToast(message || '登录已失效,请重新登录')
|
||||
|
||||
@@ -46,6 +58,7 @@ const redirectToLogin = (message: string) => {
|
||||
}
|
||||
})
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
@@ -65,9 +78,10 @@ http.interceptors.response.use(
|
||||
}
|
||||
const message = payload.msg || payload.message || '请求失败'
|
||||
if (isAuthExpired(response.status, payload.code, message, hasBearerAuthorization(response.config.headers))) {
|
||||
redirectToLogin(message)
|
||||
const toastShown = redirectToLogin(message)
|
||||
return Promise.reject(createRequestError(message, undefined, toastShown))
|
||||
}
|
||||
return Promise.reject(new Error(message))
|
||||
return Promise.reject(createRequestError(message))
|
||||
}
|
||||
return payload
|
||||
},
|
||||
@@ -81,11 +95,11 @@ http.interceptors.response.use(
|
||||
hasBearerAuthorization(error.config?.headers)
|
||||
)
|
||||
) {
|
||||
redirectToLogin(message)
|
||||
return Promise.reject(error)
|
||||
const toastShown = redirectToLogin(message)
|
||||
return Promise.reject(createRequestError(message, error, toastShown))
|
||||
}
|
||||
showToast(message)
|
||||
return Promise.reject(error)
|
||||
return Promise.reject(createRequestError(message, error, true))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ onMounted(() => loadEvents(true))
|
||||
<div>
|
||||
<p class="page-kicker">事件管理</p>
|
||||
<h1 class="page-title">事件列表</h1>
|
||||
<p class="page-subtitle">查看门店通知、弹窗提醒和正在生效的运营事件。</p>
|
||||
</div>
|
||||
<van-button type="primary" size="small" @click="router.push('/events/create')">
|
||||
<template #icon><Plus :size="16" /></template>
|
||||
@@ -96,6 +97,9 @@ onMounted(() => loadEvents(true))
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else-if="loading" class="list-skeleton">
|
||||
<van-skeleton v-for="item in 3" :key="item" title :row="4" />
|
||||
</div>
|
||||
<div v-else-if="!loading" class="empty-state">
|
||||
<Megaphone :size="30" />
|
||||
<div>暂无事件</div>
|
||||
@@ -117,10 +121,24 @@ onMounted(() => loadEvents(true))
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.list-skeleton {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.list-skeleton :deep(.van-skeleton) {
|
||||
padding: 13px;
|
||||
border: 1px solid rgba(202, 214, 208, 0.82);
|
||||
border-radius: var(--panel-radius);
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.event-card__media {
|
||||
height: 124px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid rgba(221, 230, 225, 0.9);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
@@ -133,10 +151,14 @@ onMounted(() => loadEvents(true))
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
min-width: 0;
|
||||
margin: 10px 0;
|
||||
color: var(--text-main);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.event-status-row {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import { BadgeCheck, Phone, ShieldCheck } from 'lucide-vue-next'
|
||||
import { getImageCodeUrl, sendMobileCode } from '@/api/auth'
|
||||
import type { RequestError } from '@/api/request'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -51,6 +52,11 @@ const startCountdown = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const showErrorToast = (error: unknown, fallback: string) => {
|
||||
if (error && typeof error === 'object' && (error as RequestError).toastShown) return
|
||||
showToast(error instanceof Error ? error.message : fallback)
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
showToast('请输入正确手机号')
|
||||
@@ -78,7 +84,7 @@ const handleSend = async () => {
|
||||
}
|
||||
startCountdown()
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '验证码发送失败')
|
||||
showErrorToast(error, '验证码发送失败')
|
||||
refreshImageCode()
|
||||
} finally {
|
||||
sending.value = false
|
||||
@@ -100,7 +106,7 @@ const handleLogin = async () => {
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/home'
|
||||
await router.replace(redirect)
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '登录失败')
|
||||
showErrorToast(error, '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ const logout = () => {
|
||||
<template #icon><ShieldCheck :size="18" class="cell-icon" /></template>
|
||||
</van-cell>
|
||||
<van-cell title="组织绑定" :value="auth.hasOrganization ? '已绑定' : '未绑定'" />
|
||||
<van-cell title="组织ID" :value="auth.memberInfo?.organizationId || '-'" />
|
||||
<van-cell title="员工ID" :value="auth.memberInfo?.memberId || '-'" />
|
||||
<van-cell class="id-cell" title="组织ID" :value="auth.memberInfo?.organizationId || '-'" />
|
||||
<van-cell class="id-cell" title="员工ID" :value="auth.memberInfo?.memberId || '-'" />
|
||||
<van-cell title="租户" :value="String(auth.user?.tenantId || '-')" />
|
||||
</section>
|
||||
|
||||
@@ -48,13 +48,17 @@ const logout = () => {
|
||||
gap: 12px;
|
||||
padding: 18px 14px;
|
||||
margin-top: 18px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(19, 144, 111, 0.12), rgba(237, 246, 255, 0.72)),
|
||||
var(--surface);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: grid;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid rgba(19, 144, 111, 0.16);
|
||||
border-radius: var(--panel-radius);
|
||||
background: var(--primary-soft);
|
||||
color: var(--primary-deep);
|
||||
place-items: center;
|
||||
@@ -76,6 +80,33 @@ const logout = () => {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mine-panel :deep(.van-cell) {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mine-panel :deep(.van-cell__title) {
|
||||
flex: 0 0 86px;
|
||||
color: var(--text-main);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.mine-panel :deep(.van-cell__value) {
|
||||
min-width: 0;
|
||||
color: var(--text-muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mine-panel :deep(.id-cell .van-cell__value) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.cell-icon {
|
||||
margin: 2px 8px 0 0;
|
||||
color: var(--primary);
|
||||
@@ -83,5 +114,6 @@ const logout = () => {
|
||||
|
||||
.logout-button {
|
||||
margin-top: 14px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { CalendarDays, CheckCircle2, Phone, ReceiptText, UserRound } from 'lucide-vue-next'
|
||||
import { CalendarDays, CheckCircle2, ReceiptText, UserRound } from 'lucide-vue-next'
|
||||
import { fetchOrderDetail } from '@/api/orders'
|
||||
import PageNav from '@/components/PageNav.vue'
|
||||
import StatusTag from '@/components/StatusTag.vue'
|
||||
@@ -82,10 +82,6 @@ onMounted(loadDetail)
|
||||
<span>订单金额</span>
|
||||
<strong>¥{{ detail.orderAmt }}</strong>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>优惠金额</span>
|
||||
<strong>¥{{ detail.discountAmt || '0.00' }}</strong>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>支付流水</span>
|
||||
<strong>{{ detail.paySerialNumber || '-' }}</strong>
|
||||
@@ -103,11 +99,6 @@ onMounted(loadDetail)
|
||||
<span>预约时间</span>
|
||||
<strong>{{ detail.reservationDate || detail.checkInData || '无需预约' }}</strong>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<Phone :size="17" />
|
||||
<span>投诉电话</span>
|
||||
<strong>{{ detail.complaintHotline || '-' }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel detail-panel">
|
||||
@@ -130,7 +121,8 @@ onMounted(loadDetail)
|
||||
<div>
|
||||
<strong>{{ record.commodityName || detail.commodityName }}</strong>
|
||||
<p v-if="record.packageName">套餐名称:{{ record.packageName }}</p>
|
||||
<p>{{ record.createTime }} · {{ record.userName || '员工' }}</p>
|
||||
<p>核销时间:{{ record.createTime || '-' }}</p>
|
||||
<p>核销人:{{ record.userName || '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +151,10 @@ onMounted(loadDetail)
|
||||
display: grid;
|
||||
grid-template-columns: 112px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
padding: 13px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(19, 144, 111, 0.08), rgba(237, 246, 255, 0.58)),
|
||||
var(--surface);
|
||||
}
|
||||
|
||||
.order-hero__content,
|
||||
@@ -171,6 +166,7 @@ onMounted(loadDetail)
|
||||
.order-hero img {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
border: 1px solid rgba(221, 230, 225, 0.9);
|
||||
border-radius: var(--radius);
|
||||
object-fit: cover;
|
||||
}
|
||||
@@ -181,6 +177,7 @@ onMounted(loadDetail)
|
||||
margin: 12px 0;
|
||||
color: var(--text-strong);
|
||||
font-size: 18px;
|
||||
font-weight: 750;
|
||||
line-height: 1.35;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -205,6 +202,16 @@ onMounted(loadDetail)
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.package-row,
|
||||
.record-row {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.package-row + .package-row,
|
||||
.record-row + .record-row {
|
||||
border-top: 1px solid rgba(221, 230, 225, 0.8);
|
||||
}
|
||||
|
||||
.info-row svg,
|
||||
.package-row svg,
|
||||
.record-row svg {
|
||||
@@ -234,4 +241,27 @@ onMounted(loadDetail)
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-line {
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-soft);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.order-hero {
|
||||
grid-template-columns: 92px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.order-hero img {
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
}
|
||||
|
||||
.order-hero h1 {
|
||||
margin: 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user