优化指南菜单页面样式
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
"filePath": "文档路径",
|
||||
"updateTime": "更新时间"
|
||||
},
|
||||
"catalog": {
|
||||
"title": "目录",
|
||||
"empty": "暂无目录"
|
||||
},
|
||||
"image": {
|
||||
"zoom": "点击查看原图"
|
||||
},
|
||||
"message": {
|
||||
"loadFailed": "加载操作指南失败",
|
||||
"saveSuccess": "保存成功",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
## 菜单简单介绍
|
||||
|
||||
## 工作台/统计页面:统计数据
|
||||
### 工作台/统计页面:统计数据
|
||||
|
||||

|
||||
|
||||
## 角色管理:对角色的菜单权限设置
|
||||
### 角色管理:对角色的菜单权限设置
|
||||
|
||||
按等级设定,等级越低权限越少(不要出现上级角色没有的权限,子角色有)
|
||||
避免方式:使用子角色创建下级角色,可以避免下级角色比上级角色操作权限更多的问题
|
||||
@@ -17,13 +17,13 @@
|
||||
|
||||

|
||||
|
||||
## 彩金池配置:监听彩金池实时变化
|
||||
### 彩金池配置:监听彩金池实时变化
|
||||
|
||||
可以实时监听彩金池累积金额的变化
|
||||
|
||||

|
||||
|
||||
## 游戏配置:游戏规则和平台币转化比
|
||||
### 游戏配置:游戏规则和平台币转化比
|
||||
|
||||
游戏配置
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
游戏平台币兑换币:为进入平台时平台比转化比,比如,当前设置的为1:1,如果从jk8平台转入100,那么获取的游戏币为100,如果设置1:2则获取的平台币为200
|
||||
|
||||
## 底注配置:方便玩家快速调整压注倍率
|
||||
### 底注配置:方便玩家快速调整压注倍率
|
||||
|
||||
底注配置
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
|
||||

|
||||
|
||||
# 抽奖逻辑
|
||||
## 抽奖逻辑
|
||||
|
||||
## 判断抽奖档位
|
||||
### 判断抽奖档位
|
||||
|
||||
当前的抽奖逻辑时,按照抽奖档位(T1-T5)进行抽奖,在【玩家管理】菜单中的设置玩家具体的档位权重
|
||||
|
||||
@@ -69,9 +69,9 @@
|
||||
|
||||

|
||||
|
||||
## 根据档位抽取中奖号码
|
||||
### 根据档位抽取中奖号码
|
||||
|
||||
### 设置中奖号码地图
|
||||
#### 设置中奖号码地图
|
||||
|
||||
在后台设置地图缩影
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||

|
||||
|
||||
### 创建完地图索引后创建相应的奖励对照表
|
||||
#### 创建完地图索引后创建相应的奖励对照表
|
||||
|
||||
创建奖励对照表的原因是由于有每个号码的权重不一样,豹子号10,15,20,25有多重组合方式,所以需要设置奖励对照表中的权重配比
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||

|
||||
|
||||
### 扩展
|
||||
#### 扩展
|
||||
|
||||
- 测试设置的中奖概率,根据如下设置可以测试当前设置权重的中奖概率,该测试数据不记录到真实数据系统中
|
||||
|
||||
|
||||
Reference in New Issue
Block a user