1.新增菜单后台操作指南,方便管理员查看使用
This commit is contained in:
26
saiadmin-artd/src/api/system/admin_guide.ts
Normal file
26
saiadmin-artd/src/api/system/admin_guide.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 后台操作指南 API
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* 读取 Markdown 内容
|
||||
*/
|
||||
read() {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/adminGuide/read'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存 Markdown 内容
|
||||
*/
|
||||
save(params: { content: string }) {
|
||||
return request.post<Api.Common.ApiData>({
|
||||
url: '/core/adminGuide/save',
|
||||
data: params,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
}
|
||||
20
saiadmin-artd/src/locales/langs/en/system/admin_guide.json
Normal file
20
saiadmin-artd/src/locales/langs/en/system/admin_guide.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Admin Guide",
|
||||
"toolbar": {
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"meta": {
|
||||
"filePath": "File Path",
|
||||
"updateTime": "Updated At"
|
||||
},
|
||||
"message": {
|
||||
"loadFailed": "Failed to load admin guide",
|
||||
"saveSuccess": "Saved successfully",
|
||||
"saveFailed": "Failed to save",
|
||||
"cancelConfirm": "You have unsaved changes. Cancel editing?",
|
||||
"editRequired": "Please click Edit before saving"
|
||||
}
|
||||
}
|
||||
20
saiadmin-artd/src/locales/langs/zh/system/admin_guide.json
Normal file
20
saiadmin-artd/src/locales/langs/zh/system/admin_guide.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "后台操作指南",
|
||||
"toolbar": {
|
||||
"edit": "编辑",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"meta": {
|
||||
"filePath": "文档路径",
|
||||
"updateTime": "更新时间"
|
||||
},
|
||||
"message": {
|
||||
"loadFailed": "加载操作指南失败",
|
||||
"saveSuccess": "保存成功",
|
||||
"saveFailed": "保存失败",
|
||||
"cancelConfirm": "当前有未保存的修改,确定取消编辑吗?",
|
||||
"editRequired": "请先点击编辑后再保存"
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ const NO_CHANNEL_LAYOUT_PATHS = [
|
||||
'/safeguard/database',
|
||||
'/safeguard/server',
|
||||
'/safeguard/cache',
|
||||
'/safeguard/email-log'
|
||||
'/safeguard/email-log',
|
||||
'/admin_guide'
|
||||
]
|
||||
|
||||
/** 日志页:左侧首项为「全部」,dept_id=0 表示不按渠道过滤 */
|
||||
|
||||
211
saiadmin-artd/src/views/system/admin_guide/index.vue
Normal file
211
saiadmin-artd/src/views/system/admin_guide/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="art-full-height admin-guide-page">
|
||||
<ElCard class="art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<b>{{ $t('page.title') }}</b>
|
||||
<div v-if="meta.filePath" class="mt-1 text-xs text-g-500">
|
||||
{{ $t('page.meta.filePath') }}:{{ meta.filePath }}
|
||||
<span v-if="meta.updateTime" class="ml-3">
|
||||
{{ $t('page.meta.updateTime') }}:{{ meta.updateTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'system:admin_guide:index:read'"
|
||||
:loading="loading"
|
||||
@click="loadContent"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:refresh-line" />
|
||||
</template>
|
||||
{{ $t('page.toolbar.refresh') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="!isEditing"
|
||||
v-permission="'system:admin_guide:index:edit'"
|
||||
type="primary"
|
||||
@click="startEdit"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:pencil-line" />
|
||||
</template>
|
||||
{{ $t('page.toolbar.edit') }}
|
||||
</ElButton>
|
||||
<template v-if="isEditing">
|
||||
<ElButton @click="handleCancel">
|
||||
{{ $t('page.toolbar.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'system:admin_guide:index:save'"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="handleSave"
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:save-line" />
|
||||
</template>
|
||||
{{ $t('page.toolbar.save') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElSpace>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="loading" class="admin-guide-body flex-1 min-h-0 overflow-auto">
|
||||
<SaMdEditor
|
||||
v-if="isEditing"
|
||||
v-model="editContent"
|
||||
height="calc(100vh - 220px)"
|
||||
min-height="480px"
|
||||
/>
|
||||
<MdPreview
|
||||
v-else
|
||||
:model-value="previewContent"
|
||||
:theme="previewTheme"
|
||||
preview-theme="github"
|
||||
class="admin-guide-preview"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { 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'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import api from '@/api/system/admin_guide'
|
||||
|
||||
defineOptions({ name: 'SystemAdminGuide' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const previewContent = ref('')
|
||||
const editContent = ref('')
|
||||
const originalContent = ref('')
|
||||
const meta = ref<{ filePath?: string; updateTime?: string }>({})
|
||||
|
||||
const previewTheme = computed(() => (settingStore.isDark ? 'dark' : 'light'))
|
||||
|
||||
/** 静态资源根地址(public 目录,开发环境走代理地址) */
|
||||
const getStaticFileBase = (): string => {
|
||||
const apiUrl = import.meta.env.VITE_API_URL || ''
|
||||
if (apiUrl.startsWith('http')) {
|
||||
return apiUrl.replace(/\/$/, '')
|
||||
}
|
||||
const proxyUrl = import.meta.env.VITE_API_PROXY_URL || ''
|
||||
if (proxyUrl) {
|
||||
return String(proxyUrl).replace(/\/$/, '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveGuideImages = (content: string): string => {
|
||||
const staticBase = getStaticFileBase()
|
||||
if (!staticBase) {
|
||||
return content
|
||||
}
|
||||
return content.replace(/!\[([^\]]*)\]\((\/docs\/picture\/[^)]+)\)/g, (_match, alt, path) => {
|
||||
return ``
|
||||
})
|
||||
}
|
||||
|
||||
const loadContent = async () => {
|
||||
loading.value = true
|
||||
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
|
||||
meta.value = {
|
||||
filePath: data.file_path,
|
||||
updateTime: data.update_time
|
||||
}
|
||||
} catch {
|
||||
// 错误由 http 工具统一处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startEdit = () => {
|
||||
editContent.value = previewContent.value
|
||||
originalContent.value = previewContent.value
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (editContent.value !== originalContent.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm(t('page.message.cancelConfirm'), {
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
editContent.value = originalContent.value
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isEditing.value) {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await api.save({ content: editContent.value })
|
||||
const data = res as { content?: string; file_path?: string; update_time?: string }
|
||||
const savedContent = data.content ?? editContent.value
|
||||
editContent.value = savedContent
|
||||
originalContent.value = savedContent
|
||||
previewContent.value = resolveGuideImages(savedContent)
|
||||
meta.value = {
|
||||
filePath: data.file_path ?? meta.value.filePath,
|
||||
updateTime: data.update_time ?? meta.value.updateTime
|
||||
}
|
||||
isEditing.value = false
|
||||
} catch {
|
||||
// 错误由 http 工具统一处理
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadContent()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-guide-page {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-guide-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-guide-preview {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user