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