支持配置后台页面入口
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
前端地址:`http://localhost:5173`
|
前端地址:`http://localhost:5173`
|
||||||
|
|
||||||
后台地址:`http://localhost:5173/admin/login`,开发态默认管理员为 `admin / admin123456`。
|
后台地址默认:`http://localhost:5173/admin/login`。可通过 `VITE_ADMIN_BASE_PATH` 调整后台页面入口。
|
||||||
|
|
||||||
如需拆分调试,可手动执行:
|
如需拆分调试,可手动执行:
|
||||||
|
|
||||||
@@ -55,19 +55,19 @@ npm run dev
|
|||||||
- 文件上传已接入 MinIO,发布账号资产截图和申诉证据可上传 JPG、PNG、WebP 或 PDF。
|
- 文件上传已接入 MinIO,发布账号资产截图和申诉证据可上传 JPG、PNG、WebP 或 PDF。
|
||||||
- 如果本机有代理工具占用 `127.0.0.1:9000`,MinIO 开发地址使用 `http://localhost:9000`。
|
- 如果本机有代理工具占用 `127.0.0.1:9000`,MinIO 开发地址使用 `http://localhost:9000`。
|
||||||
- 钱包账务当前为开发态模拟流水,可通过 `GET /api/wallet/balance` 和 `GET /api/wallet/ledger` 查看。
|
- 钱包账务当前为开发态模拟流水,可通过 `GET /api/wallet/balance` 和 `GET /api/wallet/ledger` 查看。
|
||||||
- 后台资金流水已接入,页面为 `http://localhost:5173/admin/wallet-ledger`,支持按用户、订单和业务类型查询。
|
- 后台资金流水已接入,支持按用户、订单和业务类型查询。
|
||||||
- 站内信已支持订单关键节点自动写入,可通过 `GET /api/notifications` 查看。
|
- 站内信已支持订单关键节点自动写入,可通过 `GET /api/notifications` 查看。
|
||||||
- 订单群聊已支持支付成功后自动创建,租客、号主和客服可在移动端消息页进入会话。
|
- 订单群聊已支持支付成功后自动创建,租客、号主和客服可在移动端消息页进入会话。
|
||||||
- 申诉仲裁已支持订单双方发起申诉、开发态后台落账处理,后台页面为 `http://localhost:5173/admin/disputes`。
|
- 申诉仲裁已支持订单双方发起申诉、开发态后台落账处理。
|
||||||
- 系统配置已支持默认配置初始化和后台编辑,页面为 `http://localhost:5173/admin/system-configs`,更新会写入审计日志。
|
- 系统配置已支持默认配置初始化和后台编辑,更新会写入审计日志。
|
||||||
- 后台已使用独立登录、图形验证码和独立管理 UI,页面为 `http://localhost:5173/admin/login`;开发态默认管理员为 `admin / admin123456`。
|
- 后台已使用独立登录、图形验证码和独立管理 UI,页面入口由 `VITE_ADMIN_BASE_PATH` 控制。
|
||||||
- 后台仪表盘已接入真实统计数据,页面为 `http://localhost:5173/admin/dashboard`。
|
- 后台仪表盘已接入真实统计数据。
|
||||||
- 用户管理后台已接入,页面为 `http://localhost:5173/admin/users`,支持冻结和解冻用户。
|
- 用户管理后台已接入,支持冻结和解冻用户。
|
||||||
- 订单管理后台已接入,页面为 `http://localhost:5173/admin/orders`,支持查看全量订单和交接记录。
|
- 订单管理后台已接入,支持查看全量订单和交接记录。
|
||||||
- 商品管理后台已接入,页面为 `http://localhost:5173/admin/listings`,支持查看全量商品、商品详情、强制下架和标记异常。
|
- 商品管理后台已接入,支持查看全量商品、商品详情、强制下架和标记异常。
|
||||||
- 订单详情后台已支持客服关闭和标记异常,相关操作会写入审计日志并通知双方。
|
- 订单详情后台已支持客服关闭和标记异常,相关操作会写入审计日志并通知双方。
|
||||||
- 商品审核后台已接入,页面为 `http://localhost:5173/admin/listings/review`,审核员可查看账号资产截图后通过或拒绝。
|
- 商品审核后台已接入,审核员可查看账号资产截图后通过或拒绝。
|
||||||
- 审计日志后台已接入,页面为 `http://localhost:5173/admin/audit-logs`,支持查看高风险操作明细。
|
- 审计日志后台已接入,支持查看高风险操作明细。
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ REDIS_DB=0
|
|||||||
|
|
||||||
JWT_SECRET=change-me
|
JWT_SECRET=change-me
|
||||||
|
|
||||||
|
# 前端后台页面入口;本地默认 /admin,生产建议改成不公开的单段路径。
|
||||||
|
VITE_ADMIN_BASE_PATH=/admin
|
||||||
|
|
||||||
# 首次启动且管理员表为空时,按以下变量创建首个超级管理员。
|
# 首次启动且管理员表为空时,按以下变量创建首个超级管理员。
|
||||||
# 本地开发可自行填写,生产环境请使用随机强密码,创建后建议从 .env 移除。
|
# 本地开发可自行填写,生产环境请使用随机强密码,创建后建议从 .env 移除。
|
||||||
ADMIN_BOOTSTRAP_USERNAME=
|
ADMIN_BOOTSTRAP_USERNAME=
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ REDIS_DB=0
|
|||||||
|
|
||||||
JWT_SECRET=change-to-a-long-random-secret
|
JWT_SECRET=change-to-a-long-random-secret
|
||||||
|
|
||||||
|
# 前端后台页面入口;生产请改成不公开的单段路径,不要使用 /admin。
|
||||||
|
VITE_ADMIN_BASE_PATH=/manage-7x9k2p
|
||||||
|
|
||||||
# 首次启动且管理员表为空时创建首个超级管理员;生产密码至少 12 位且包含字母和数字。
|
# 首次启动且管理员表为空时创建首个超级管理员;生产密码至少 12 位且包含字母和数字。
|
||||||
# 创建成功后建议从 .env 移除,后续管理员通过后台维护。
|
# 创建成功后建议从 .env 移除,后续管理员通过后台维护。
|
||||||
ADMIN_BOOTSTRAP_USERNAME=change-admin-username
|
ADMIN_BOOTSTRAP_USERNAME=change-admin-username
|
||||||
|
|||||||
+2
-2
@@ -108,8 +108,8 @@ curl https://你的域名/api/health
|
|||||||
浏览器访问:
|
浏览器访问:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
https://hfb.221329.cc.cd
|
https://你的域名
|
||||||
https://hfb.221329.cc.cd/admin/login
|
https://你的域名/你配置的后台入口/login
|
||||||
```
|
```
|
||||||
|
|
||||||
Caddy 证书和 ACME 账号数据保存在 Docker 卷 `caddy_data`、`caddy_config` 中。不要随意删除这两个卷,否则 Caddy 会重新申请证书,频繁重建可能触发证书签发频率限制。
|
Caddy 证书和 ACME 账号数据保存在 Docker 卷 `caddy_data`、`caddy_config` 中。不要随意删除这两个卷,否则 Caddy 会重新申请证书,频繁重建可能触发证书签发频率限制。
|
||||||
|
|||||||
@@ -48,6 +48,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 生产后台页面使用构建时配置的隐藏入口,旧 /admin 入口直接返回 404。
|
||||||
|
@legacy_admin path /admin /admin/*
|
||||||
|
handle @legacy_admin {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
|
||||||
# 其余流量返回前端静态资源,未命中的路径交给 SPA 处理。
|
# 其余流量返回前端静态资源,未命中的路径交给 SPA 处理。
|
||||||
handle {
|
handle {
|
||||||
root * /srv/dist
|
root * /srv/dist
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# 第一阶段:构建前端静态资源
|
# 第一阶段:构建前端静态资源
|
||||||
FROM node:24-alpine AS build
|
FROM node:24-alpine AS build
|
||||||
|
|
||||||
|
ARG VITE_ADMIN_BASE_PATH=/manage-7x9k2p
|
||||||
|
ENV VITE_ADMIN_BASE_PATH=${VITE_ADMIN_BASE_PATH}
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: deploy/caddy/Dockerfile
|
dockerfile: deploy/caddy/Dockerfile
|
||||||
|
args:
|
||||||
|
VITE_ADMIN_BASE_PATH: ${VITE_ADMIN_BASE_PATH:-/manage-7x9k2p}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import ChatAttachmentImage from '@/components/ChatAttachmentImage.vue'
|
|||||||
import { useChatSSE, type ChatEvent } from '@/features/chats/composables/useChatSSE'
|
import { useChatSSE, type ChatEvent } from '@/features/chats/composables/useChatSSE'
|
||||||
import { useDesktopNotification } from '@/features/chats/composables/useDesktopNotification'
|
import { useDesktopNotification } from '@/features/chats/composables/useDesktopNotification'
|
||||||
import NotificationSettings from '@/features/chats/components/NotificationSettings.vue'
|
import NotificationSettings from '@/features/chats/components/NotificationSettings.vue'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { formatDateMinute } from '@/shared/utils/time'
|
import { formatDateMinute } from '@/shared/utils/time'
|
||||||
import TransferDialog from '../components/TransferDialog.vue'
|
import TransferDialog from '../components/TransferDialog.vue'
|
||||||
import QuickReplyDialog from '../components/QuickReplyDialog.vue'
|
import QuickReplyDialog from '../components/QuickReplyDialog.vue'
|
||||||
@@ -407,7 +408,7 @@ function getSupportName(item: ChatConversation) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<el-button size="small" @click="transferVisible = true">转接</el-button>
|
<el-button size="small" @click="transferVisible = true">转接</el-button>
|
||||||
<RouterLink v-if="active.order_id" :to="`/admin/orders/${active.order_id}`">
|
<RouterLink v-if="active.order_id" :to="adminPath(`orders/${active.order_id}`)">
|
||||||
<el-button size="small">查看订单</el-button>
|
<el-button size="small">查看订单</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard'
|
import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard'
|
||||||
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
|
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { disputeStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
|
import { disputeStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ const {
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="pending-list">
|
<div class="pending-list">
|
||||||
<RouterLink class="pending-row" to="/admin/disputes">
|
<RouterLink class="pending-row" :to="adminPath('disputes')">
|
||||||
<div class="pending-info">
|
<div class="pending-info">
|
||||||
<span class="pending-label">待仲裁申诉</span>
|
<span class="pending-label">待仲裁申诉</span>
|
||||||
<span class="pending-desc">需要处理的用户争议</span>
|
<span class="pending-desc">需要处理的用户争议</span>
|
||||||
@@ -130,7 +131,7 @@ const {
|
|||||||
dashboard.pending.disputes
|
dashboard.pending.disputes
|
||||||
}}</strong>
|
}}</strong>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink class="pending-row" to="/admin/listings/review">
|
<RouterLink class="pending-row" :to="adminPath('listings/review')">
|
||||||
<div class="pending-info">
|
<div class="pending-info">
|
||||||
<span class="pending-label">待审核商品</span>
|
<span class="pending-label">待审核商品</span>
|
||||||
<span class="pending-desc">新提交的商品审核</span>
|
<span class="pending-desc">新提交的商品审核</span>
|
||||||
@@ -168,27 +169,27 @@ const {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-links">
|
<div class="quick-links">
|
||||||
<RouterLink class="quick-link" to="/admin/users">
|
<RouterLink class="quick-link" :to="adminPath('users')">
|
||||||
<el-icon><User /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<span>用户管理</span>
|
<span>用户管理</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink class="quick-link" to="/admin/orders">
|
<RouterLink class="quick-link" :to="adminPath('orders')">
|
||||||
<el-icon><Tickets /></el-icon>
|
<el-icon><Tickets /></el-icon>
|
||||||
<span>订单管理</span>
|
<span>订单管理</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink class="quick-link" to="/admin/disputes">
|
<RouterLink class="quick-link" :to="adminPath('disputes')">
|
||||||
<el-icon><ScaleToOriginal /></el-icon>
|
<el-icon><ScaleToOriginal /></el-icon>
|
||||||
<span>仲裁中心</span>
|
<span>仲裁中心</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink class="quick-link" to="/admin/listings">
|
<RouterLink class="quick-link" :to="adminPath('listings')">
|
||||||
<el-icon><Shop /></el-icon>
|
<el-icon><Shop /></el-icon>
|
||||||
<span>商品管理</span>
|
<span>商品管理</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink class="quick-link" to="/admin/chats">
|
<RouterLink class="quick-link" :to="adminPath('chats')">
|
||||||
<el-icon><ChatDotRound /></el-icon>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
<span>客服群聊</span>
|
<span>客服群聊</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink class="quick-link" to="/admin/system-configs">
|
<RouterLink class="quick-link" :to="adminPath('system-configs')">
|
||||||
<el-icon><Operation /></el-icon>
|
<el-icon><Operation /></el-icon>
|
||||||
<span>系统配置</span>
|
<span>系统配置</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@@ -259,7 +260,7 @@ const {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100">
|
<el-table-column label="操作" width="100">
|
||||||
<template #default>
|
<template #default>
|
||||||
<RouterLink to="/admin/disputes">
|
<RouterLink :to="adminPath('disputes')">
|
||||||
<el-button size="small" type="primary" link>处理</el-button>
|
<el-button size="small" type="primary" link>处理</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Search } from '@element-plus/icons-vue'
|
|||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchFinanceDetails, type FinanceDetail } from '@/features/admin/api/adminFinance'
|
import { fetchFinanceDetails, type FinanceDetail } from '@/features/admin/api/adminFinance'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
import { orderStatusLabel } from '@/shared/utils/statusLabels'
|
import { orderStatusLabel } from '@/shared/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
@@ -197,7 +198,7 @@ function formatInputDate(date: Date) {
|
|||||||
<div v-if="details.length > 0" class="finance-grid-body">
|
<div v-if="details.length > 0" class="finance-grid-body">
|
||||||
<div v-for="row in details" :key="row.order_id" class="finance-grid-row">
|
<div v-for="row in details" :key="row.order_id" class="finance-grid-row">
|
||||||
<div class="order-cell">
|
<div class="order-cell">
|
||||||
<RouterLink class="order-no" :to="`/admin/orders/${row.order_id}`">
|
<RouterLink class="order-no" :to="adminPath(`orders/${row.order_id}`)">
|
||||||
{{ row.order_no }}
|
{{ row.order_no }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<span class="cell-muted">结算 {{ shortDateTime(row.settled_at) }}</span>
|
<span class="cell-muted">结算 {{ shortDateTime(row.settled_at) }}</span>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
import {
|
import {
|
||||||
adminMarkListingAbnormal,
|
adminMarkListingAbnormal,
|
||||||
@@ -117,7 +118,7 @@ async function openScreenshot(url: string) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-button :icon="CopyDocument" @click="copyListingCode">复制编号</el-button>
|
<el-button :icon="CopyDocument" @click="copyListingCode">复制编号</el-button>
|
||||||
<RouterLink to="/admin/listings">
|
<RouterLink :to="adminPath('listings')">
|
||||||
<el-button>返回列表</el-button>
|
<el-button>返回列表</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<el-button type="warning" :disabled="!canOperate" @click="openAction('offline')"
|
<el-button type="warning" :disabled="!canOperate" @click="openAction('offline')"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { centToYuan, formatCent, formatMoneyWithSymbol, yuanToCent } from '@/shared/utils/money'
|
import { centToYuan, formatCent, formatMoneyWithSymbol, yuanToCent } from '@/shared/utils/money'
|
||||||
import {
|
import {
|
||||||
adjustListingReviewPrice,
|
adjustListingReviewPrice,
|
||||||
@@ -667,7 +668,7 @@ async function openScreenshot(url: string) {
|
|||||||
<h2>{{ selectedListing.title }}</h2>
|
<h2>{{ selectedListing.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<RouterLink :to="`/admin/listings/${selectedListing.id}`">
|
<RouterLink :to="adminPath(`listings/${selectedListing.id}`)">
|
||||||
<el-button>详情页</el-button>
|
<el-button>详情页</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<el-button
|
<el-button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type Listing,
|
type Listing,
|
||||||
} from '@/features/listings'
|
} from '@/features/listings'
|
||||||
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
|
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
import {
|
import {
|
||||||
assetRegions,
|
assetRegions,
|
||||||
@@ -558,7 +559,7 @@ function formatQuantity(value: number) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="详情" width="60">
|
<el-table-column label="详情" width="60">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<RouterLink :to="`/admin/listings/${row.id}`">
|
<RouterLink :to="adminPath(`listings/${row.id}`)">
|
||||||
<el-button size="small">详情</el-button>
|
<el-button size="small">详情</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { fetchAdminCaptcha, type AdminCaptcha } from '@/features/admin'
|
import { fetchAdminCaptcha, type AdminCaptcha } from '@/features/admin'
|
||||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||||
|
import { ADMIN_DASHBOARD_PATH, isAdminPath } from '@/shared/utils/adminPath'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -45,7 +46,7 @@ async function handleLogin() {
|
|||||||
)
|
)
|
||||||
ElMessage.success('后台登录成功')
|
ElMessage.success('后台登录成功')
|
||||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : ''
|
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : ''
|
||||||
await router.push(redirect.startsWith('/admin') ? redirect : '/admin/dashboard')
|
await router.push(isAdminPath(redirect) ? redirect : ADMIN_DASHBOARD_PATH)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(readError(error, '后台登录失败'))
|
ElMessage.error(readError(error, '后台登录失败'))
|
||||||
await loadCaptcha()
|
await loadCaptcha()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type RefundStatus,
|
type RefundStatus,
|
||||||
} from '@/features/orders'
|
} from '@/features/orders'
|
||||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { centToYuan, formatCentWithSymbol, formatMoney } from '@/shared/utils/money'
|
import { centToYuan, formatCentWithSymbol, formatMoney } from '@/shared/utils/money'
|
||||||
import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
|
import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
@@ -195,7 +196,7 @@ function formatHandoffRecordType(type: string) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<RouterLink to="/admin/orders">
|
<RouterLink :to="adminPath('orders')">
|
||||||
<el-button>返回列表</el-button>
|
<el-button>返回列表</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<el-button type="warning" :disabled="!canOperate" @click="openAction('abnormal')"
|
<el-button type="warning" :disabled="!canOperate" @click="openAction('abnormal')"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
import { centToYuan, formatMoney } from '@/shared/utils/money'
|
import { centToYuan, formatMoney } from '@/shared/utils/money'
|
||||||
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||||
|
|
||||||
const status = ref('')
|
const status = ref('')
|
||||||
@@ -109,7 +110,7 @@ function money(value: unknown) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100">
|
<el-table-column label="操作" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<RouterLink :to="`/admin/orders/${row.id}`">
|
<RouterLink :to="adminPath(`orders/${row.id}`)">
|
||||||
<el-button size="small">详情</el-button>
|
<el-button size="small">详情</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Document, Search } from '@element-plus/icons-vue'
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||||
@@ -196,7 +197,7 @@ function jsonText(value: unknown) {
|
|||||||
<el-table-column prop="payment_no" label="支付单号" min-width="220" />
|
<el-table-column prop="payment_no" label="支付单号" min-width="220" />
|
||||||
<el-table-column label="订单" min-width="180">
|
<el-table-column label="订单" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<RouterLink v-if="row.order_id" :to="`/admin/orders/${row.order_id}`">{{
|
<RouterLink v-if="row.order_id" :to="adminPath(`orders/${row.order_id}`)">{{
|
||||||
row.order_no || row.order_id
|
row.order_no || row.order_id
|
||||||
}}</RouterLink>
|
}}</RouterLink>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Search } from '@element-plus/icons-vue'
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/features/admin/api/adminWallet'
|
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/features/admin/api/adminWallet'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
import { balanceTypeLabel, ledgerDirectionLabel } from '@/shared/utils/statusLabels'
|
import { balanceTypeLabel, ledgerDirectionLabel } from '@/shared/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
@@ -136,7 +137,7 @@ function directionLabel(direction: string) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="订单" min-width="160">
|
<el-table-column label="订单" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<RouterLink v-if="row.order_id" :to="`/admin/orders/${row.order_id}`">{{
|
<RouterLink v-if="row.order_id" :to="adminPath(`orders/${row.order_id}`)">{{
|
||||||
row.order_no || row.order_id
|
row.order_no || row.order_id
|
||||||
}}</RouterLink>
|
}}</RouterLink>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { ChatEvent } from './useChatSSE'
|
import type { ChatEvent } from './useChatSSE'
|
||||||
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { debugError } from '@/shared/utils/debug'
|
import { debugError } from '@/shared/utils/debug'
|
||||||
|
|
||||||
export type NotificationPermission = 'default' | 'granted' | 'denied'
|
export type NotificationPermission = 'default' | 'granted' | 'denied'
|
||||||
@@ -109,7 +110,7 @@ export function useDesktopNotification(scope: 'user' | 'admin') {
|
|||||||
window.focus()
|
window.focus()
|
||||||
const path =
|
const path =
|
||||||
scope === 'admin'
|
scope === 'admin'
|
||||||
? `/admin/chats/${event.conversation_id}`
|
? adminPath(`chats/${event.conversation_id}`)
|
||||||
: `/messages/${event.conversation_id}`
|
: `/messages/${event.conversation_id}`
|
||||||
router.push(path)
|
router.push(path)
|
||||||
notification.close()
|
notification.close()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { changeAdminPassword, logoutAdmin, updateSupportStatus } from '@/features/admin'
|
import { changeAdminPassword, logoutAdmin, updateSupportStatus } from '@/features/admin'
|
||||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||||
|
import { adminPath, ADMIN_DASHBOARD_PATH, ADMIN_LOGIN_PATH } from '@/shared/utils/adminPath'
|
||||||
import { readError } from '@/shared/utils/error'
|
import { readError } from '@/shared/utils/error'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -66,7 +67,7 @@ const allNavGroups: NavGroup[] = [
|
|||||||
index: 'workspace',
|
index: 'workspace',
|
||||||
icon: House,
|
icon: House,
|
||||||
children: [
|
children: [
|
||||||
{ label: '仪表盘', to: '/admin/dashboard', icon: DataLine, permission: 'dashboard:view' },
|
{ label: '仪表盘', to: ADMIN_DASHBOARD_PATH, icon: DataLine, permission: 'dashboard:view' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -74,12 +75,12 @@ const allNavGroups: NavGroup[] = [
|
|||||||
index: 'business',
|
index: 'business',
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
children: [
|
children: [
|
||||||
{ label: '用户管理', to: '/admin/users', icon: User, permission: 'user:view' },
|
{ label: '用户管理', to: adminPath('users'), icon: User, permission: 'user:view' },
|
||||||
{ label: '订单管理', to: '/admin/orders', icon: Tickets, permission: 'order:view' },
|
{ label: '订单管理', to: adminPath('orders'), icon: Tickets, permission: 'order:view' },
|
||||||
{ label: '商品管理', to: '/admin/listings', icon: Shop, permission: 'listing:view' },
|
{ label: '商品管理', to: adminPath('listings'), icon: Shop, permission: 'listing:view' },
|
||||||
{
|
{
|
||||||
label: '商品审核',
|
label: '商品审核',
|
||||||
to: '/admin/listings/review',
|
to: adminPath('listings/review'),
|
||||||
icon: DocumentChecked,
|
icon: DocumentChecked,
|
||||||
permission: 'listing:approve',
|
permission: 'listing:approve',
|
||||||
},
|
},
|
||||||
@@ -92,11 +93,11 @@ const allNavGroups: NavGroup[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: '仲裁中心',
|
label: '仲裁中心',
|
||||||
to: '/admin/disputes',
|
to: adminPath('disputes'),
|
||||||
icon: ScaleToOriginal,
|
icon: ScaleToOriginal,
|
||||||
permission: 'dispute:view',
|
permission: 'dispute:view',
|
||||||
},
|
},
|
||||||
{ label: '客服群聊', to: '/admin/chats', icon: ChatDotRound, permission: 'chat:view' },
|
{ label: '客服群聊', to: adminPath('chats'), icon: ChatDotRound, permission: 'chat:view' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -106,22 +107,22 @@ const allNavGroups: NavGroup[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: '财务仪表盘',
|
label: '财务仪表盘',
|
||||||
to: '/admin/finance/dashboard',
|
to: adminPath('finance/dashboard'),
|
||||||
icon: DataLine,
|
icon: DataLine,
|
||||||
permission: 'wallet:view',
|
permission: 'wallet:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '财务明细',
|
label: '财务明细',
|
||||||
to: '/admin/finance/details',
|
to: adminPath('finance/details'),
|
||||||
icon: Document,
|
icon: Document,
|
||||||
permission: 'wallet:view',
|
permission: 'wallet:view',
|
||||||
},
|
},
|
||||||
{ label: '资金流水', to: '/admin/wallet-ledger', icon: Wallet, permission: 'wallet:view' },
|
{ label: '资金流水', to: adminPath('wallet-ledger'), icon: Wallet, permission: 'wallet:view' },
|
||||||
{ label: '支付流水', to: '/admin/payments', icon: CreditCard, permission: 'wallet:view' },
|
{ label: '支付流水', to: adminPath('payments'), icon: CreditCard, permission: 'wallet:view' },
|
||||||
{ label: '提现审核', to: '/admin/withdrawals', icon: Money, permission: 'withdrawal:list' },
|
{ label: '提现审核', to: adminPath('withdrawals'), icon: Money, permission: 'withdrawal:list' },
|
||||||
{
|
{
|
||||||
label: '支付配置',
|
label: '支付配置',
|
||||||
to: '/admin/payment-configs',
|
to: adminPath('payment-configs'),
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
permission: 'payment_config:list',
|
permission: 'payment_config:list',
|
||||||
},
|
},
|
||||||
@@ -134,13 +135,13 @@ const allNavGroups: NavGroup[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: '公告管理',
|
label: '公告管理',
|
||||||
to: '/admin/announcements',
|
to: adminPath('announcements'),
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
permission: 'announcement:view',
|
permission: 'announcement:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '系统配置',
|
label: '系统配置',
|
||||||
to: '/admin/system-configs',
|
to: adminPath('system-configs'),
|
||||||
icon: Operation,
|
icon: Operation,
|
||||||
permission: 'system_config:view',
|
permission: 'system_config:view',
|
||||||
},
|
},
|
||||||
@@ -151,14 +152,14 @@ const allNavGroups: NavGroup[] = [
|
|||||||
index: 'system-auth',
|
index: 'system-auth',
|
||||||
icon: Lock,
|
icon: Lock,
|
||||||
children: [
|
children: [
|
||||||
{ label: '审计日志', to: '/admin/audit-logs', icon: Document, permission: 'audit_log:view' },
|
{ label: '审计日志', to: adminPath('audit-logs'), icon: Document, permission: 'audit_log:view' },
|
||||||
{
|
{
|
||||||
label: '管理员管理',
|
label: '管理员管理',
|
||||||
to: '/admin/admin-users',
|
to: adminPath('admin-users'),
|
||||||
icon: UserFilled,
|
icon: UserFilled,
|
||||||
permission: 'admin_user:manage',
|
permission: 'admin_user:manage',
|
||||||
},
|
},
|
||||||
{ label: '角色管理', to: '/admin/roles', icon: Setting, permission: 'role:manage' },
|
{ label: '角色管理', to: adminPath('roles'), icon: Setting, permission: 'role:manage' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -236,7 +237,7 @@ async function handleLogout() {
|
|||||||
}
|
}
|
||||||
adminSession.logout()
|
adminSession.logout()
|
||||||
ElMessage.success('已退出后台')
|
ElMessage.success('已退出后台')
|
||||||
await router.push('/admin/login')
|
await router.push(ADMIN_LOGIN_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
function passwordStrongEnough(value: string) {
|
function passwordStrongEnough(value: string) {
|
||||||
@@ -264,7 +265,7 @@ async function handleForcedPasswordChange() {
|
|||||||
})
|
})
|
||||||
ElMessage.success('密码已修改,请重新登录')
|
ElMessage.success('密码已修改,请重新登录')
|
||||||
adminSession.logout()
|
adminSession.logout()
|
||||||
await router.replace('/admin/login')
|
await router.replace(ADMIN_LOGIN_PATH)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(readError(error, '密码修改失败'))
|
ElMessage.error(readError(error, '密码修改失败'))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -277,7 +278,7 @@ async function handleForcedPasswordChange() {
|
|||||||
<div class="admin-shell" :class="{ 'admin-shell--collapsed': isCollapsed }">
|
<div class="admin-shell" :class="{ 'admin-shell--collapsed': isCollapsed }">
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
<div class="admin-sidebar-header">
|
<div class="admin-sidebar-header">
|
||||||
<RouterLink class="admin-brand" to="/admin/dashboard">
|
<RouterLink class="admin-brand" :to="ADMIN_DASHBOARD_PATH">
|
||||||
<span class="brand-mark">H</span>
|
<span class="brand-mark">H</span>
|
||||||
<span class="brand-text" v-show="!isCollapsed">哈夫币后台</span>
|
<span class="brand-text" v-show="!isCollapsed">哈夫币后台</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@@ -318,7 +319,7 @@ async function handleForcedPasswordChange() {
|
|||||||
<header class="admin-topbar">
|
<header class="admin-topbar">
|
||||||
<div class="topbar-left">
|
<div class="topbar-left">
|
||||||
<el-breadcrumb separator="/">
|
<el-breadcrumb separator="/">
|
||||||
<el-breadcrumb-item :to="{ path: '/admin/dashboard' }">首页</el-breadcrumb-item>
|
<el-breadcrumb-item :to="{ path: ADMIN_DASHBOARD_PATH }">首页</el-breadcrumb-item>
|
||||||
<el-breadcrumb-item v-if="route.meta.title">{{ route.meta.title }}</el-breadcrumb-item>
|
<el-breadcrumb-item v-if="route.meta.title">{{ route.meta.title }}</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,131 +1,133 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
import { adminPath, ADMIN_DASHBOARD_PATH } from '@/shared/utils/adminPath'
|
||||||
|
|
||||||
const adminMeta = { layout: 'admin', requiresAdmin: true }
|
const adminMeta = { layout: 'admin', requiresAdmin: true }
|
||||||
|
|
||||||
export const adminRoutes: RouteRecordRaw[] = [
|
export const adminRoutes: RouteRecordRaw[] = [
|
||||||
{ path: '/admin', redirect: '/admin/dashboard' },
|
{ path: adminPath(), redirect: ADMIN_DASHBOARD_PATH },
|
||||||
{
|
{
|
||||||
path: '/admin/login',
|
path: adminPath('login'),
|
||||||
name: 'admin-login',
|
name: 'admin-login',
|
||||||
component: () => import('@/features/admin/views/AdminLoginView.vue'),
|
component: () => import('@/features/admin/views/AdminLoginView.vue'),
|
||||||
meta: { layout: 'blank' },
|
meta: { layout: 'blank' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/dashboard',
|
path: adminPath('dashboard'),
|
||||||
name: 'admin-dashboard',
|
name: 'admin-dashboard',
|
||||||
component: () => import('@/features/admin/views/AdminDashboardView.vue'),
|
component: () => import('@/features/admin/views/AdminDashboardView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/users',
|
path: adminPath('users'),
|
||||||
name: 'admin-users',
|
name: 'admin-users',
|
||||||
component: () => import('@/features/admin/views/AdminUsersView.vue'),
|
component: () => import('@/features/admin/views/AdminUsersView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/orders',
|
path: adminPath('orders'),
|
||||||
name: 'admin-orders',
|
name: 'admin-orders',
|
||||||
component: () => import('@/features/admin/views/AdminOrdersView.vue'),
|
component: () => import('@/features/admin/views/AdminOrdersView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/orders/:id',
|
path: adminPath('orders/:id'),
|
||||||
name: 'admin-order-detail',
|
name: 'admin-order-detail',
|
||||||
component: () => import('@/features/admin/views/AdminOrderDetailView.vue'),
|
component: () => import('@/features/admin/views/AdminOrderDetailView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/listings',
|
path: adminPath('listings'),
|
||||||
name: 'admin-listings',
|
name: 'admin-listings',
|
||||||
component: () => import('@/features/admin/views/AdminListingsView.vue'),
|
component: () => import('@/features/admin/views/AdminListingsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/listings/:id',
|
path: adminPath('listings/:id'),
|
||||||
name: 'admin-listing-detail',
|
name: 'admin-listing-detail',
|
||||||
component: () => import('@/features/admin/views/AdminListingDetailView.vue'),
|
component: () => import('@/features/admin/views/AdminListingDetailView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/listings/review',
|
path: adminPath('listings/review'),
|
||||||
name: 'admin-listing-review',
|
name: 'admin-listing-review',
|
||||||
component: () => import('@/features/admin/views/AdminListingReviewView.vue'),
|
component: () => import('@/features/admin/views/AdminListingReviewView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/disputes',
|
path: adminPath('disputes'),
|
||||||
name: 'admin-disputes',
|
name: 'admin-disputes',
|
||||||
component: () => import('@/features/admin/views/AdminDisputesView.vue'),
|
component: () => import('@/features/admin/views/AdminDisputesView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/chats',
|
path: adminPath('chats'),
|
||||||
name: 'admin-chats',
|
name: 'admin-chats',
|
||||||
component: () => import('@/features/admin/views/AdminChatsView.vue'),
|
component: () => import('@/features/admin/views/AdminChatsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/finance/dashboard',
|
path: adminPath('finance/dashboard'),
|
||||||
name: 'admin-finance-dashboard',
|
name: 'admin-finance-dashboard',
|
||||||
component: () => import('@/features/admin/views/AdminFinanceDashboardView.vue'),
|
component: () => import('@/features/admin/views/AdminFinanceDashboardView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/finance/details',
|
path: adminPath('finance/details'),
|
||||||
name: 'admin-finance-details',
|
name: 'admin-finance-details',
|
||||||
component: () => import('@/features/admin/views/AdminFinanceDetailsView.vue'),
|
component: () => import('@/features/admin/views/AdminFinanceDetailsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/wallet-ledger',
|
path: adminPath('wallet-ledger'),
|
||||||
name: 'admin-wallet-ledger',
|
name: 'admin-wallet-ledger',
|
||||||
component: () => import('@/features/admin/views/AdminWalletLedgerView.vue'),
|
component: () => import('@/features/admin/views/AdminWalletLedgerView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/payments',
|
path: adminPath('payments'),
|
||||||
name: 'admin-payments',
|
name: 'admin-payments',
|
||||||
component: () => import('@/features/admin/views/AdminPaymentsView.vue'),
|
component: () => import('@/features/admin/views/AdminPaymentsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/withdrawals',
|
path: adminPath('withdrawals'),
|
||||||
name: 'admin-withdrawals',
|
name: 'admin-withdrawals',
|
||||||
component: () => import('@/features/admin/views/AdminWithdrawalsView.vue'),
|
component: () => import('@/features/admin/views/AdminWithdrawalsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/system-configs',
|
path: adminPath('system-configs'),
|
||||||
name: 'admin-system-configs',
|
name: 'admin-system-configs',
|
||||||
component: () => import('@/features/admin/views/AdminSystemConfigsView.vue'),
|
component: () => import('@/features/admin/views/AdminSystemConfigsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/payment-configs',
|
path: adminPath('payment-configs'),
|
||||||
name: 'admin-payment-configs',
|
name: 'admin-payment-configs',
|
||||||
component: () => import('@/features/admin/views/AdminPaymentConfigsView.vue'),
|
component: () => import('@/features/admin/views/AdminPaymentConfigsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/audit-logs',
|
path: adminPath('audit-logs'),
|
||||||
name: 'admin-audit-logs',
|
name: 'admin-audit-logs',
|
||||||
component: () => import('@/features/admin/views/AdminAuditLogsView.vue'),
|
component: () => import('@/features/admin/views/AdminAuditLogsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/admin-users',
|
path: adminPath('admin-users'),
|
||||||
name: 'admin-admin-users',
|
name: 'admin-admin-users',
|
||||||
component: () => import('@/features/admin/views/AdminMgrUsersView.vue'),
|
component: () => import('@/features/admin/views/AdminMgrUsersView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/roles',
|
path: adminPath('roles'),
|
||||||
name: 'admin-roles',
|
name: 'admin-roles',
|
||||||
component: () => import('@/features/admin/views/AdminRolesView.vue'),
|
component: () => import('@/features/admin/views/AdminRolesView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/announcements',
|
path: adminPath('announcements'),
|
||||||
name: 'admin-announcements',
|
name: 'admin-announcements',
|
||||||
component: () => import('@/features/admin/views/AdminAnnouncementsView.vue'),
|
component: () => import('@/features/admin/views/AdminAnnouncementsView.vue'),
|
||||||
meta: adminMeta,
|
meta: adminMeta,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { getAccessToken, getLoginPath } from '@/shared/utils/authStorage'
|
import { getAccessToken, getLoginPath } from '@/shared/utils/authStorage'
|
||||||
|
import { ADMIN_DASHBOARD_PATH, ADMIN_LOGIN_PATH } from '@/shared/utils/adminPath'
|
||||||
import { accountRoutes } from './accountRoutes'
|
import { accountRoutes } from './accountRoutes'
|
||||||
import { adminRoutes } from './adminRoutes'
|
import { adminRoutes } from './adminRoutes'
|
||||||
import { isMobileHost, isMobileDevice, getMobilePath, getPcPath } from './mobileHost'
|
import { isMobileHost, isMobileDevice, getMobilePath, getPcPath } from './mobileHost'
|
||||||
@@ -77,11 +78,11 @@ router.beforeEach(async to => {
|
|||||||
|
|
||||||
const adminSession = useAdminSessionStore()
|
const adminSession = useAdminSessionStore()
|
||||||
adminSession.syncFromStorage()
|
adminSession.syncFromStorage()
|
||||||
if (to.path === '/admin/login') {
|
if (to.path === ADMIN_LOGIN_PATH) {
|
||||||
if (!adminSession.hasSessionHint) return true
|
if (!adminSession.hasSessionHint) return true
|
||||||
try {
|
try {
|
||||||
await adminSession.loadMe()
|
await adminSession.loadMe()
|
||||||
return '/admin/dashboard'
|
return ADMIN_DASHBOARD_PATH
|
||||||
} catch {
|
} catch {
|
||||||
adminSession.logout()
|
adminSession.logout()
|
||||||
return true
|
return true
|
||||||
@@ -98,7 +99,7 @@ router.beforeEach(async to => {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
adminSession.logout()
|
adminSession.logout()
|
||||||
return { path: '/admin/login', query: { redirect: to.fullPath } }
|
return { path: ADMIN_LOGIN_PATH, query: { redirect: to.fullPath } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isAdminPath } from '@/shared/utils/adminPath'
|
||||||
|
|
||||||
export function isMobileHost(hostname = window.location.hostname) {
|
export function isMobileHost(hostname = window.location.hostname) {
|
||||||
return hostname === 'm.localhost' || hostname.startsWith('m.')
|
return hostname === 'm.localhost' || hostname.startsWith('m.')
|
||||||
}
|
}
|
||||||
@@ -14,7 +16,7 @@ export function getMobilePath(path: string): string {
|
|||||||
if (path === '/' || path === '') {
|
if (path === '/' || path === '') {
|
||||||
return '/m'
|
return '/m'
|
||||||
}
|
}
|
||||||
if (path === '/m' || path.startsWith('/m/') || path.startsWith('/admin')) {
|
if (path === '/m' || path.startsWith('/m/') || isAdminPath(path)) {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ export function getMobilePath(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPcPath(path: string): string {
|
export function getPcPath(path: string): string {
|
||||||
if ((path !== '/m' && !path.startsWith('/m/')) || path.startsWith('/admin')) {
|
if ((path !== '/m' && !path.startsWith('/m/')) || isAdminPath(path)) {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const DEFAULT_ADMIN_BASE_PATH = '/admin'
|
||||||
|
|
||||||
|
function normalizeAdminBasePath(value: string | undefined) {
|
||||||
|
const raw = (value || DEFAULT_ADMIN_BASE_PATH).trim()
|
||||||
|
if (!raw || raw === '/') return DEFAULT_ADMIN_BASE_PATH
|
||||||
|
const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`
|
||||||
|
return withLeadingSlash.replace(/\/+$/, '') || DEFAULT_ADMIN_BASE_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_BASE_PATH = normalizeAdminBasePath(import.meta.env.VITE_ADMIN_BASE_PATH)
|
||||||
|
|
||||||
|
export function adminPath(path = '') {
|
||||||
|
const suffix = path.trim().replace(/^\/+/, '')
|
||||||
|
if (!suffix) return ADMIN_BASE_PATH
|
||||||
|
return `${ADMIN_BASE_PATH}/${suffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_LOGIN_PATH = adminPath('login')
|
||||||
|
export const ADMIN_DASHBOARD_PATH = adminPath('dashboard')
|
||||||
|
|
||||||
|
export function isAdminPath(path: string) {
|
||||||
|
return path === ADMIN_BASE_PATH || path.startsWith(`${ADMIN_BASE_PATH}/`)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ADMIN_LOGIN_PATH } from '@/shared/utils/adminPath'
|
||||||
|
|
||||||
export type AuthScope = 'user' | 'admin'
|
export type AuthScope = 'user' | 'admin'
|
||||||
|
|
||||||
export interface AuthTokenPair {
|
export interface AuthTokenPair {
|
||||||
@@ -53,7 +55,7 @@ export function clearAuthStorage(scope: AuthScope) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getLoginPath(scope: AuthScope, currentPath: string) {
|
export function getLoginPath(scope: AuthScope, currentPath: string) {
|
||||||
if (scope === 'admin') return '/admin/login'
|
if (scope === 'admin') return ADMIN_LOGIN_PATH
|
||||||
return currentPath.startsWith('/m') ? '/m/login' : '/login'
|
return currentPath.startsWith('/m') ? '/m/login' : '/login'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -127,13 +127,14 @@ validate_env() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local app_env caddy_domain caddy_email mysql_dsn redis_addr storage_endpoint
|
local app_env caddy_domain caddy_email mysql_dsn redis_addr storage_endpoint admin_base_path
|
||||||
app_env="$(require_env APP_ENV)"
|
app_env="$(require_env APP_ENV)"
|
||||||
caddy_domain="$(require_env CADDY_DOMAIN)"
|
caddy_domain="$(require_env CADDY_DOMAIN)"
|
||||||
caddy_email="$(require_env CADDY_EMAIL)"
|
caddy_email="$(require_env CADDY_EMAIL)"
|
||||||
mysql_dsn="$(require_env MYSQL_DSN)"
|
mysql_dsn="$(require_env MYSQL_DSN)"
|
||||||
redis_addr="$(require_env REDIS_ADDR)"
|
redis_addr="$(require_env REDIS_ADDR)"
|
||||||
storage_endpoint="$(require_env STORAGE_ENDPOINT)"
|
storage_endpoint="$(require_env STORAGE_ENDPOINT)"
|
||||||
|
admin_base_path="$(require_env VITE_ADMIN_BASE_PATH)"
|
||||||
require_env MYSQL_DATABASE >/dev/null
|
require_env MYSQL_DATABASE >/dev/null
|
||||||
require_env MYSQL_ROOT_PASSWORD >/dev/null
|
require_env MYSQL_ROOT_PASSWORD >/dev/null
|
||||||
require_env MYSQL_USER >/dev/null
|
require_env MYSQL_USER >/dev/null
|
||||||
@@ -173,6 +174,14 @@ validate_env() {
|
|||||||
log_error "STORAGE_ENDPOINT 仍指向本机,请改为 http://minio:9000"
|
log_error "STORAGE_ENDPOINT 仍指向本机,请改为 http://minio:9000"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ ! "${admin_base_path}" =~ ^/[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
||||||
|
log_error "VITE_ADMIN_BASE_PATH 必须是单段路径,例如 /manage-7x9k2p,当前值:${admin_base_path}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "${admin_base_path}" == "/admin" || "${admin_base_path}" == "/m" || "${admin_base_path}" == "/api" ]]; then
|
||||||
|
log_error "VITE_ADMIN_BASE_PATH 不能使用 ${admin_base_path},请改成不公开的后台入口"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
if awk -F= '/^[A-Z0-9_]+=/{ if ($0 ~ /=(change-|保持你的|你的)/) found=1 } END { exit found ? 0 : 1 }' "${BACKEND_ENV}"; then
|
if awk -F= '/^[A-Z0-9_]+=/{ if ($0 ~ /=(change-|保持你的|你的)/) found=1 } END { exit found ? 0 : 1 }' "${BACKEND_ENV}"; then
|
||||||
log_error "backend/.env 里还有占位值,请先替换后再部署"
|
log_error "backend/.env 里还有占位值,请先替换后再部署"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -186,8 +195,10 @@ compose() {
|
|||||||
export_caddy_env() {
|
export_caddy_env() {
|
||||||
export CADDY_DOMAIN
|
export CADDY_DOMAIN
|
||||||
export CADDY_EMAIL
|
export CADDY_EMAIL
|
||||||
|
export VITE_ADMIN_BASE_PATH
|
||||||
CADDY_DOMAIN="$(require_env CADDY_DOMAIN)"
|
CADDY_DOMAIN="$(require_env CADDY_DOMAIN)"
|
||||||
CADDY_EMAIL="$(require_env CADDY_EMAIL)"
|
CADDY_EMAIL="$(require_env CADDY_EMAIL)"
|
||||||
|
VITE_ADMIN_BASE_PATH="$(require_env VITE_ADMIN_BASE_PATH)"
|
||||||
}
|
}
|
||||||
|
|
||||||
public_url() {
|
public_url() {
|
||||||
|
|||||||
+2
-1
@@ -587,8 +587,9 @@ main() {
|
|||||||
|
|
||||||
log_success "开发环境已启动"
|
log_success "开发环境已启动"
|
||||||
if [[ "${NO_FRONTEND}" == "0" ]]; then
|
if [[ "${NO_FRONTEND}" == "0" ]]; then
|
||||||
|
local admin_base_path="${VITE_ADMIN_BASE_PATH:-/admin}"
|
||||||
log "前台:http://localhost:5173"
|
log "前台:http://localhost:5173"
|
||||||
log "后台:http://localhost:5173/admin/login"
|
log "后台:http://localhost:5173${admin_base_path%/}/login"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "按 Ctrl+C 停止前后端;Docker 依赖会保留运行"
|
log "按 Ctrl+C 停止前后端;Docker 依赖会保留运行"
|
||||||
|
|||||||
Reference in New Issue
Block a user