优化指南菜单页面样式

This commit is contained in:
2026-05-30 18:41:53 +08:00
parent 90abab14a3
commit 18944f0d48
4 changed files with 378 additions and 26 deletions

View File

@@ -10,6 +10,13 @@
"filePath": "File Path",
"updateTime": "Updated At"
},
"catalog": {
"title": "Catalog",
"empty": "No headings"
},
"image": {
"zoom": "Click to view full size"
},
"message": {
"loadFailed": "Failed to load admin guide",
"saveSuccess": "Saved successfully",

View File

@@ -10,6 +10,13 @@
"filePath": "文档路径",
"updateTime": "更新时间"
},
"catalog": {
"title": "目录",
"empty": "暂无目录"
},
"image": {
"zoom": "点击查看原图"
},
"message": {
"loadFailed": "加载操作指南失败",
"saveSuccess": "保存成功",

View File

@@ -54,29 +54,73 @@
</div>
</template>
<div v-loading="loading" class="admin-guide-body flex-1 min-h-0 overflow-auto">
<div v-loading="loading" class="admin-guide-body flex-1 min-h-0">
<SaMdEditor
v-if="isEditing"
v-model="editContent"
class="admin-guide-editor"
height="calc(100vh - 220px)"
min-height="480px"
/>
<MdPreview
v-else
:model-value="previewContent"
:theme="previewTheme"
preview-theme="github"
class="admin-guide-preview"
/>
<div v-else class="admin-guide-read flex h-full min-h-0">
<aside class="admin-guide-catalog flex-shrink-0">
<div class="catalog-title">{{ $t('page.catalog.title') }}</div>
<ElScrollbar class="catalog-scroll">
<nav v-if="tocList.length" class="guide-toc">
<button
v-for="(item, index) in tocList"
:key="`${item.level}-${item.text}-${index}`"
type="button"
class="guide-toc-item"
:class="[
`guide-toc-level-${item.level}`,
{ 'is-active': activeTocIndex === index }
]"
:title="item.text"
@click="scrollToHeading(item, index)"
>
{{ item.text }}
</button>
</nav>
<div v-else class="guide-toc-empty">{{ $t('page.catalog.empty') }}</div>
</ElScrollbar>
</aside>
<div
id="admin-guide-scroll"
ref="previewScrollRef"
class="admin-guide-preview-wrap flex-1 min-h-0 overflow-auto"
>
<MdPreview
:editor-id="previewEditorId"
:model-value="previewContent"
:theme="previewTheme"
preview-theme="github"
no-img-zoom-in
class="admin-guide-preview"
@on-html-changed="handlePreviewHtmlReady"
/>
</div>
</div>
</div>
</ElCard>
<ElImageViewer
v-if="imageViewerVisible"
:key="imageViewerIndex"
:url-list="previewImageUrls"
:initial-index="imageViewerIndex"
:hide-on-click-modal="true"
:z-index="3000"
teleported
@close="closeImageViewer"
/>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessageBox } from 'element-plus'
import { ElImageViewer, ElMessageBox } from 'element-plus'
import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/preview.css'
import SaMdEditor from '@/components/sai/sa-md-editor/index.vue'
@@ -85,9 +129,16 @@
defineOptions({ name: 'SystemAdminGuide' })
interface GuideTocItem {
level: number
text: string
}
const { t } = useI18n()
const settingStore = useSettingStore()
const previewEditorId = 'admin-guide-preview'
const previewScrollRef = ref<HTMLElement | null>(null)
const loading = ref(false)
const saving = ref(false)
const isEditing = ref(false)
@@ -95,10 +146,17 @@
const editContent = ref('')
const originalContent = ref('')
const meta = ref<{ filePath?: string; updateTime?: string }>({})
const tocList = ref<GuideTocItem[]>([])
const activeTocIndex = ref(-1)
const previewImageUrls = ref<string[]>([])
const imageViewerVisible = ref(false)
const imageViewerIndex = ref(0)
let previewImageClickHandler: ((event: Event) => void) | null = null
let previewEnhanceTimer: ReturnType<typeof setTimeout> | null = null
const previewTheme = computed(() => (settingStore.isDark ? 'dark' : 'light'))
/** 静态资源根地址public 目录,开发环境走代理地址) */
const getStaticFileBase = (): string => {
const apiUrl = import.meta.env.VITE_API_URL || ''
if (apiUrl.startsWith('http')) {
@@ -121,19 +179,159 @@
})
}
const parseTocFromMarkdown = (content: string): GuideTocItem[] => {
const items: GuideTocItem[] = []
for (const line of content.split('\n')) {
const match = line.match(/^(#{1,6})\s+(.+)$/)
if (!match) {
continue
}
items.push({
level: match[1].length,
text: match[2].trim()
})
}
return items
}
const refreshTocList = () => {
tocList.value = parseTocFromMarkdown(originalContent.value)
activeTocIndex.value = -1
}
const collectPreviewImages = () => {
const container = previewScrollRef.value
if (!container) {
previewImageUrls.value = []
return
}
const imgs = container.querySelectorAll('img')
previewImageUrls.value = Array.from(imgs).map((img) => {
const el = img as HTMLImageElement
return el.currentSrc || el.src
})
}
const findImageIndex = (img: HTMLImageElement): number => {
collectPreviewImages()
const targetSrc = img.currentSrc || img.src
let index = previewImageUrls.value.indexOf(targetSrc)
if (index >= 0) {
return index
}
index = previewImageUrls.value.findIndex((url) => targetSrc.endsWith(url) || url.endsWith(targetSrc))
if (index >= 0) {
return index
}
previewImageUrls.value = [...previewImageUrls.value, targetSrc]
return previewImageUrls.value.length - 1
}
const openImageViewerByImg = (img: HTMLImageElement) => {
const index = findImageIndex(img)
openImageViewer(index)
}
const openImageViewer = (index: number) => {
if (index < 0 || index >= previewImageUrls.value.length) {
return
}
imageViewerIndex.value = index
imageViewerVisible.value = true
}
const closeImageViewer = () => {
imageViewerVisible.value = false
}
const unbindPreviewImageClick = (clearTimer = true) => {
const container = previewScrollRef.value
if (container && previewImageClickHandler) {
container.removeEventListener('click', previewImageClickHandler, true)
}
previewImageClickHandler = null
if (clearTimer && previewEnhanceTimer) {
clearTimeout(previewEnhanceTimer)
previewEnhanceTimer = null
}
}
const bindPreviewImageClick = () => {
unbindPreviewImageClick(false)
const container = previewScrollRef.value
if (!container) {
return
}
collectPreviewImages()
previewImageClickHandler = (event: Event) => {
const imgEl = (event.target as Element | null)?.closest('img')
if (!(imgEl instanceof HTMLImageElement)) {
return
}
if (!container.contains(imgEl)) {
return
}
event.preventDefault()
event.stopPropagation()
openImageViewerByImg(imgEl)
}
container.addEventListener('click', previewImageClickHandler, true)
const imgs = container.querySelectorAll('img')
imgs.forEach((img) => {
const el = img as HTMLImageElement
el.style.cursor = 'zoom-in'
el.title = t('page.image.zoom')
})
}
const handlePreviewHtmlReady = () => {
if (previewEnhanceTimer) {
clearTimeout(previewEnhanceTimer)
}
previewEnhanceTimer = setTimeout(() => {
bindPreviewImageClick()
previewEnhanceTimer = null
}, 50)
}
const setupPreviewEnhancements = async () => {
await nextTick()
refreshTocList()
handlePreviewHtmlReady()
}
const scrollToHeading = (item: GuideTocItem, index: number) => {
const container = previewScrollRef.value
if (!container) {
return
}
const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6')
for (const heading of Array.from(headings)) {
if (heading.textContent?.trim() === item.text) {
activeTocIndex.value = index
heading.scrollIntoView({ behavior: 'smooth', block: 'start' })
return
}
}
}
const loadContent = async () => {
loading.value = true
unbindPreviewImageClick()
try {
const res = await api.read()
const data = res as { content?: string; file_path?: string; update_time?: string }
const rawContent = data.content ?? ''
previewContent.value = resolveGuideImages(rawContent)
editContent.value = rawContent
originalContent.value = rawContent
previewContent.value = resolveGuideImages(rawContent)
meta.value = {
filePath: data.file_path,
updateTime: data.update_time
}
await setupPreviewEnhancements()
} catch {
// 错误由 http 工具统一处理
} finally {
@@ -142,9 +340,9 @@
}
const startEdit = () => {
editContent.value = previewContent.value
originalContent.value = previewContent.value
editContent.value = originalContent.value
isEditing.value = true
unbindPreviewImageClick()
}
const handleCancel = async () => {
@@ -159,6 +357,7 @@
}
editContent.value = originalContent.value
isEditing.value = false
await setupPreviewEnhancements()
}
const handleSave = async () => {
@@ -178,6 +377,7 @@
updateTime: data.update_time ?? meta.value.updateTime
}
isEditing.value = false
await setupPreviewEnhancements()
} catch {
// 错误由 http 工具统一处理
} finally {
@@ -185,9 +385,20 @@
}
}
watch(previewContent, async () => {
if (!isEditing.value) {
await nextTick()
handlePreviewHtmlReady()
}
})
onMounted(() => {
loadContent()
})
onBeforeUnmount(() => {
unbindPreviewImageClick()
})
</script>
<style scoped lang="scss">
@@ -203,9 +414,136 @@
.admin-guide-body {
width: 100%;
min-height: 0;
}
.admin-guide-read {
min-height: 0;
gap: 0;
}
.admin-guide-catalog {
display: flex;
flex-direction: column;
width: 240px;
min-height: 0;
padding: 4px 12px 12px 4px;
border-right: 1px solid var(--el-border-color-lighter);
}
.catalog-title {
flex-shrink: 0;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.catalog-scroll {
flex: 1;
min-height: 0;
}
.guide-toc {
display: flex;
flex-direction: column;
gap: 2px;
padding-right: 8px;
}
.guide-toc-item {
display: block;
width: 100%;
padding: 5px 8px;
overflow: hidden;
font-size: 13px;
line-height: 1.5;
color: var(--el-text-color-regular);
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
background: transparent;
border: none;
border-radius: 4px;
transition: color 0.2s, background-color 0.2s;
&:hover,
&.is-active {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
.guide-toc-level-1 {
padding-left: 8px;
font-weight: 600;
}
.guide-toc-level-2 {
padding-left: 20px;
}
.guide-toc-level-3 {
padding-left: 32px;
font-size: 12px;
}
.guide-toc-level-4 {
padding-left: 44px;
font-size: 12px;
}
.guide-toc-level-5,
.guide-toc-level-6 {
padding-left: 56px;
font-size: 12px;
}
.guide-toc-empty {
padding: 8px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.admin-guide-preview-wrap {
position: relative;
padding: 8px 12px 8px 16px;
}
.admin-guide-preview {
padding: 8px 4px;
padding: 0;
}
.admin-guide-editor {
width: 100%;
}
@media (max-width: 768px) {
.admin-guide-read {
flex-direction: column;
}
.admin-guide-catalog {
width: 100%;
max-height: 200px;
padding-right: 0;
border-right: none;
border-bottom: 1px solid var(--el-border-color-lighter);
}
}
</style>
<style lang="scss">
.admin-guide-preview-wrap .md-editor-preview-wrapper img,
.admin-guide-preview-wrap .md-editor-preview img,
.admin-guide-editor .md-editor-preview-wrapper img,
.admin-guide-editor .md-editor-preview img {
display: block;
width: 50% !important;
max-width: 50% !important;
height: auto !important;
margin: 8px 0;
cursor: zoom-in;
}
</style>

View File

@@ -2,11 +2,11 @@
## 菜单简单介绍
## 工作台/统计页面:统计数据
### 工作台/统计页面:统计数据
![image.png](/docs/picture/guide_01.png)
## 角色管理:对角色的菜单权限设置
### 角色管理:对角色的菜单权限设置
按等级设定,等级越低权限越少(不要出现上级角色没有的权限,子角色有)
避免方式:使用子角色创建下级角色,可以避免下级角色比上级角色操作权限更多的问题
@@ -17,13 +17,13 @@
![image.png](/docs/picture/guide_03.png)
## 彩金池配置:监听彩金池实时变化
### 彩金池配置:监听彩金池实时变化
可以实时监听彩金池累积金额的变化
![image.png](/docs/picture/guide_04.png)
## 游戏配置:游戏规则和平台币转化比
### 游戏配置:游戏规则和平台币转化比
游戏配置
@@ -35,7 +35,7 @@
游戏平台币兑换币为进入平台时平台比转化比比如当前设置的为11如果从jk8平台转入100那么获取的游戏币为100如果设置12则获取的平台币为200
## 底注配置:方便玩家快速调整压注倍率
### 底注配置:方便玩家快速调整压注倍率
底注配置
@@ -45,9 +45,9 @@
![image.png](/docs/picture/guide_08.png)
# 抽奖逻辑
## 抽奖逻辑
## 判断抽奖档位
### 判断抽奖档位
当前的抽奖逻辑时按照抽奖档位T1-T5进行抽奖在【玩家管理】菜单中的设置玩家具体的档位权重
@@ -69,9 +69,9 @@
![image.png](/docs/picture/guide_13.png)
## 根据档位抽取中奖号码
### 根据档位抽取中奖号码
### 设置中奖号码地图
#### 设置中奖号码地图
在后台设置地图缩影
@@ -89,7 +89,7 @@
![image.png](/docs/picture/guide_17.png)
### 创建完地图索引后创建相应的奖励对照表
#### 创建完地图索引后创建相应的奖励对照表
创建奖励对照表的原因是由于有每个号码的权重不一样豹子号10152025有多重组合方式所以需要设置奖励对照表中的权重配比
@@ -107,7 +107,7 @@
![image.png](/docs/picture/guide_21.png)
### 扩展
#### 扩展
- 测试设置的中奖概率,根据如下设置可以测试当前设置权重的中奖概率,该测试数据不记录到真实数据系统中