feat(admin): 首页编辑支持链接选择器(本站页面/他站首页/文件)与试跳
Made-with: Cursor
This commit is contained in:
@@ -2,34 +2,65 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
title="选择链接"
|
title="选择链接"
|
||||||
width="640px"
|
width="680px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
@update:model-value="$emit('update:modelValue', $event)"
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
>
|
>
|
||||||
<el-tabs v-model="tab">
|
<el-tabs v-model="tab">
|
||||||
<el-tab-pane label="站内页面" name="pages">
|
<el-tab-pane label="本站页面" name="pages">
|
||||||
|
<p class="tab-tip">当前站点下已创建的网页(含首页 /)</p>
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loadingPages"
|
v-loading="loadingPages"
|
||||||
:data="pageRows"
|
:data="pageRows"
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
max-height="360"
|
max-height="320"
|
||||||
@row-click="(row) => confirm(row.path)"
|
@row-click="(row) => confirm(row.path)"
|
||||||
>
|
>
|
||||||
<el-table-column prop="title" label="标题" min-width="140" />
|
<el-table-column prop="title" label="标题" min-width="130" />
|
||||||
<el-table-column prop="path" label="路径" min-width="160" />
|
<el-table-column prop="path" label="前台路径" min-width="140" />
|
||||||
<el-table-column label="操作" width="80">
|
<el-table-column label="操作" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click.stop="confirm(row.path)">选择</el-button>
|
<el-button type="primary" link @click.stop="confirm(row.path)">选择</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-empty v-if="!loadingPages && !pageRows.length" description="暂无页面" />
|
<el-empty v-if="!loadingPages && !pageRows.length" description="暂无页面,请先在网页管理中创建" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="可下载文件" name="files">
|
|
||||||
|
<el-tab-pane v-if="showOtherSites" label="其他站点首页" name="sites">
|
||||||
|
<p class="tab-tip">同账号下其他站点的首页链接(需在站点管理中填写「域名」)</p>
|
||||||
|
<el-table
|
||||||
|
v-loading="loadingSites"
|
||||||
|
:data="otherSiteRows"
|
||||||
|
max-height="320"
|
||||||
|
>
|
||||||
|
<el-table-column prop="name" label="站点名称" min-width="120" />
|
||||||
|
<el-table-column prop="domain" label="已配置域名" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.domain">{{ row.domain }}</span>
|
||||||
|
<el-text v-else type="warning" size="small">未填写</el-text>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="previewUrl" label="将填入的链接" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="!row.homeUrl"
|
||||||
|
@click="confirmOtherSite(row)"
|
||||||
|
>选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="!loadingSites && !otherSiteRows.length" description="没有其他站点" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane v-if="showDownloadableFiles" label="可下载文件" name="files">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loadingFiles"
|
v-loading="loadingFiles"
|
||||||
:data="files"
|
:data="files"
|
||||||
max-height="360"
|
max-height="320"
|
||||||
@row-click="(row) => confirm(fileUrl(row))"
|
@row-click="(row) => confirm(fileUrl(row))"
|
||||||
>
|
>
|
||||||
<el-table-column prop="name" label="文件名" min-width="160" />
|
<el-table-column prop="name" label="文件名" min-width="160" />
|
||||||
@@ -42,6 +73,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
<el-empty v-if="!loadingFiles && !files.length" description="无标记为可下载的文件,请到文件管理上传并勾选允许下载" />
|
<el-empty v-if="!loadingFiles && !files.length" description="无标记为可下载的文件,请到文件管理上传并勾选允许下载" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="自定义地址" name="custom">
|
<el-tab-pane label="自定义地址" name="custom">
|
||||||
<el-input v-model="customUrl" placeholder="https:// 或 /path" clearable />
|
<el-input v-model="customUrl" placeholder="https:// 或 /path" clearable />
|
||||||
<div style="margin-top: 16px">
|
<div style="margin-top: 16px">
|
||||||
@@ -55,18 +87,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getPages, getDownloadableAssets } from '../api/admin'
|
import { getPages, getDownloadableAssets, getSites } from '../api/admin'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
siteId: { type: String, default: '' }
|
siteId: { type: String, default: '' },
|
||||||
|
showDownloadableFiles: { type: Boolean, default: true },
|
||||||
|
showOtherSites: { type: Boolean, default: true }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue', 'select'])
|
const emit = defineEmits(['update:modelValue', 'select'])
|
||||||
|
|
||||||
const tab = ref('pages')
|
const tab = ref('pages')
|
||||||
const pageRows = ref([])
|
const pageRows = ref([])
|
||||||
|
const otherSiteRows = ref([])
|
||||||
const files = ref([])
|
const files = ref([])
|
||||||
const loadingPages = ref(false)
|
const loadingPages = ref(false)
|
||||||
|
const loadingSites = ref(false)
|
||||||
const loadingFiles = ref(false)
|
const loadingFiles = ref(false)
|
||||||
const customUrl = ref('')
|
const customUrl = ref('')
|
||||||
|
|
||||||
@@ -79,6 +115,14 @@ function effectivePath(p) {
|
|||||||
return '/' + p.slug
|
return '/' + p.slug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 站点首页完整 URL:依赖站点 domain;无域名则无法生成外链 */
|
||||||
|
function siteHomepageUrl(site) {
|
||||||
|
let d = (site.domain || '').trim()
|
||||||
|
if (!d) return ''
|
||||||
|
if (!/^https?:\/\//i.test(d)) d = 'https://' + d
|
||||||
|
return d.replace(/\/$/, '') + '/'
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPages() {
|
async function loadPages() {
|
||||||
if (!props.siteId) return
|
if (!props.siteId) return
|
||||||
loadingPages.value = true
|
loadingPages.value = true
|
||||||
@@ -86,7 +130,7 @@ async function loadPages() {
|
|||||||
const res = await getPages({ site_id: props.siteId })
|
const res = await getPages({ site_id: props.siteId })
|
||||||
const list = res.list || []
|
const list = res.list || []
|
||||||
pageRows.value = list.map((p) => ({
|
pageRows.value = list.map((p) => ({
|
||||||
title: p.title || p.slug,
|
title: (p.type === 'homepage' || p.slug === 'index' ? '【首页】' : '') + (p.title || p.slug),
|
||||||
path: effectivePath(p),
|
path: effectivePath(p),
|
||||||
slug: p.slug
|
slug: p.slug
|
||||||
}))
|
}))
|
||||||
@@ -97,6 +141,36 @@ async function loadPages() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadOtherSites() {
|
||||||
|
loadingSites.value = true
|
||||||
|
try {
|
||||||
|
const res = await getSites()
|
||||||
|
const list = (res.list || []).filter((s) => s.id !== props.siteId)
|
||||||
|
otherSiteRows.value = list.map((s) => {
|
||||||
|
const homeUrl = siteHomepageUrl(s)
|
||||||
|
return {
|
||||||
|
id: s.id,
|
||||||
|
name: s.name || s.id,
|
||||||
|
domain: (s.domain || '').trim(),
|
||||||
|
homeUrl,
|
||||||
|
previewUrl: homeUrl || '(未配置域名)'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
otherSiteRows.value = []
|
||||||
|
} finally {
|
||||||
|
loadingSites.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmOtherSite(row) {
|
||||||
|
if (!row.homeUrl) {
|
||||||
|
ElMessage.warning('请先在「站点管理」中为该站点填写访问域名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirm(row.homeUrl)
|
||||||
|
}
|
||||||
|
|
||||||
async function loadFiles() {
|
async function loadFiles() {
|
||||||
if (!props.siteId) return
|
if (!props.siteId) return
|
||||||
loadingFiles.value = true
|
loadingFiles.value = true
|
||||||
@@ -136,8 +210,17 @@ watch(
|
|||||||
customUrl.value = ''
|
customUrl.value = ''
|
||||||
tab.value = 'pages'
|
tab.value = 'pages'
|
||||||
loadPages()
|
loadPages()
|
||||||
loadFiles()
|
if (props.showOtherSites) loadOtherSites()
|
||||||
|
if (props.showDownloadableFiles) loadFiles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -30,9 +30,21 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="导航链接">
|
<el-form-item label="导航链接">
|
||||||
<div v-for="(link, i) in form.nav_links" :key="i" style="display: flex; gap: 8px; margin-bottom: 8px">
|
<div
|
||||||
<el-input v-model="link.label" placeholder="Label" style="width: 120px" />
|
v-for="(link, i) in form.nav_links"
|
||||||
<el-input v-model="link.url" placeholder="URL" style="flex: 1" />
|
:key="i"
|
||||||
|
style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap"
|
||||||
|
>
|
||||||
|
<el-input v-model="link.label" placeholder="显示文字" style="width: 120px" />
|
||||||
|
<el-input v-model="link.url" placeholder="路径或外链,可点「选择链接」" style="flex: 1; min-width: 160px" />
|
||||||
|
<el-button type="primary" link @click="openLinkPicker({ type: 'nav', index: i })">选择链接</el-button>
|
||||||
|
<el-link
|
||||||
|
v-if="previewReady(link.url)"
|
||||||
|
type="primary"
|
||||||
|
:href="previewHref(link.url)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>试跳</el-link>
|
||||||
<el-button link type="danger" @click="form.nav_links.splice(i, 1)">删除</el-button>
|
<el-button link type="danger" @click="form.nav_links.splice(i, 1)">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-button link type="primary" @click="form.nav_links.push({ label: '', url: '#' })">+ 添加链接</el-button>
|
<el-button link type="primary" @click="form.nav_links.push({ label: '', url: '#' })">+ 添加链接</el-button>
|
||||||
@@ -43,16 +55,32 @@
|
|||||||
<el-input v-model="form.download_text" placeholder="START EXPLORING" />
|
<el-input v-model="form.download_text" placeholder="START EXPLORING" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="按钮链接">
|
<el-form-item label="按钮链接">
|
||||||
<el-input v-model="form.download_url" placeholder="#" style="flex: 1" />
|
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap; width: 100%">
|
||||||
<el-button type="primary" link @click="openFilePicker('download')">选择可下载文件</el-button>
|
<el-input v-model="form.download_url" placeholder="#" style="flex: 1; min-width: 200px" />
|
||||||
|
<el-button type="primary" link @click="openLinkPicker({ type: 'download' })">选择链接</el-button>
|
||||||
|
<el-link
|
||||||
|
v-if="previewReady(form.download_url)"
|
||||||
|
type="primary"
|
||||||
|
:href="previewHref(form.download_url)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>试跳</el-link>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-divider content-position="left">平台(轨道)</el-divider>
|
<el-divider content-position="left">平台(轨道)</el-divider>
|
||||||
<el-form-item label="平台列表">
|
<el-form-item label="平台列表">
|
||||||
<div v-for="(p, i) in form.platforms" :key="i" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center">
|
<div v-for="(p, i) in form.platforms" :key="i" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center">
|
||||||
<el-input v-model="p.name" placeholder="如 WINDOWS" style="width: 140px" />
|
<el-input v-model="p.name" placeholder="如 WINDOWS" style="width: 140px" />
|
||||||
<el-input v-model="p.url" placeholder="链接" style="flex: 1" />
|
<el-input v-model="p.url" placeholder="链接" style="flex: 1; min-width: 140px" />
|
||||||
<el-button type="primary" link @click="openFilePicker('platform', i)">选择文件</el-button>
|
<el-button type="primary" link @click="openLinkPicker({ type: 'platform', index: i })">选择链接</el-button>
|
||||||
|
<el-link
|
||||||
|
v-if="previewReady(p.url)"
|
||||||
|
type="primary"
|
||||||
|
:href="previewHref(p.url)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>试跳</el-link>
|
||||||
<el-button link type="danger" @click="form.platforms.splice(i, 1)">删除</el-button>
|
<el-button link type="danger" @click="form.platforms.splice(i, 1)">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-button link type="primary" @click="form.platforms.push({ name: '', url: '#' })">+ 添加平台</el-button>
|
<el-button link type="primary" @click="form.platforms.push({ name: '', url: '#' })">+ 添加平台</el-button>
|
||||||
@@ -87,41 +115,29 @@
|
|||||||
<el-empty v-else description="请先选择站点" />
|
<el-empty v-else description="请先选择站点" />
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="filePickerVisible" title="选择可下载文件" width="640px">
|
<LinkPickerDialog
|
||||||
<el-table
|
v-model="linkPickerVisible"
|
||||||
v-loading="fileListLoading"
|
:site-id="siteId"
|
||||||
:data="downloadableFiles"
|
@select="onLinkPicked"
|
||||||
highlight-current-row
|
/>
|
||||||
@current-change="onSelectFile"
|
|
||||||
>
|
|
||||||
<el-table-column property="name" label="文件名" min-width="180" />
|
|
||||||
<el-table-column property="file_path" label="路径" min-width="200" show-overflow-tooltip />
|
|
||||||
<el-table-column label="操作" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="primary" link @click="confirmSelectFile(row)">选择</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-empty v-if="!fileListLoading && downloadableFiles.length === 0" description="暂无可下载文件,请在文件管理中上传并勾选“允许下载”" />
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, watch } from 'vue'
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getSites, getOfficialSite, getHomepage, updateHomepage, getDownloadableAssets } from '../../api/admin'
|
import { getSites, getOfficialSite, getHomepage, updateHomepage } from '../../api/admin'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
import LinkPickerDialog from '../../components/LinkPickerDialog.vue'
|
||||||
|
|
||||||
const siteId = ref('')
|
const siteId = ref('')
|
||||||
const sites = ref([])
|
const sites = ref([])
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const downloading = ref(false)
|
const downloading = ref(false)
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const filePickerVisible = ref(false)
|
const linkPickerVisible = ref(false)
|
||||||
const fileListLoading = ref(false)
|
/** @type {import('vue').Ref<{ type: 'nav' | 'download' | 'platform'; index?: number }>} */
|
||||||
const downloadableFiles = ref([])
|
const linkPickTarget = ref({ type: 'download' })
|
||||||
const filePickerTarget = ref({ type: 'download' }) // { type: 'download' } | { type: 'platform', index: number }
|
|
||||||
|
|
||||||
const defaultForm = () => ({
|
const defaultForm = () => ({
|
||||||
logo_text: 'YUHENG ONE',
|
logo_text: 'YUHENG ONE',
|
||||||
@@ -227,43 +243,34 @@ const handleDownload = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFilePicker(type, index) {
|
function openLinkPicker(target) {
|
||||||
filePickerTarget.value = type === 'platform' ? { type: 'platform', index } : { type: 'download' }
|
linkPickTarget.value = target
|
||||||
filePickerVisible.value = true
|
linkPickerVisible.value = true
|
||||||
fetchDownloadableFiles()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDownloadableFiles() {
|
function onLinkPicked(url) {
|
||||||
if (!siteId.value) return
|
const t = linkPickTarget.value
|
||||||
fileListLoading.value = true
|
if (t.type === 'nav' && typeof t.index === 'number') {
|
||||||
try {
|
form.nav_links[t.index].url = url
|
||||||
const res = await getDownloadableAssets(siteId.value)
|
} else if (t.type === 'download') {
|
||||||
downloadableFiles.value = res.list || []
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(e.message)
|
|
||||||
downloadableFiles.value = []
|
|
||||||
} finally {
|
|
||||||
fileListLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDownloadUrl(asset) {
|
|
||||||
return `/api/web/sites/${siteId.value}/assets/${asset.id}/download`
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmSelectFile(asset) {
|
|
||||||
const url = buildDownloadUrl(asset)
|
|
||||||
if (filePickerTarget.value.type === 'download') {
|
|
||||||
form.download_url = url
|
form.download_url = url
|
||||||
} else {
|
} else if (t.type === 'platform' && typeof t.index === 'number') {
|
||||||
form.platforms[filePickerTarget.value.index].url = url
|
form.platforms[t.index].url = url
|
||||||
}
|
}
|
||||||
filePickerVisible.value = false
|
ElMessage.success('已填入链接')
|
||||||
ElMessage.success('已选择:' + asset.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectFile(row) {
|
function previewReady(url) {
|
||||||
if (row) confirmSelectFile(row)
|
const u = (url || '').trim()
|
||||||
|
return Boolean(u && u !== '#')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后台试跳:相对路径用当前浏览器域名拼接 */
|
||||||
|
function previewHref(url) {
|
||||||
|
const u = (url || '').trim()
|
||||||
|
if (/^https?:\/\//i.test(u)) return u
|
||||||
|
if (u.startsWith('/')) return `${window.location.origin}${u}`
|
||||||
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -51,3 +51,11 @@
|
|||||||
|
|
||||||
1. 在 `web/src/components/blocks/BlockRenderer.vue` 增加 `type` 分支与样式。
|
1. 在 `web/src/components/blocks/BlockRenderer.vue` 增加 `type` 分支与样式。
|
||||||
2. 后台仍通过 JSON 配置 `props`,无需改库表结构。
|
2. 后台仍通过 JSON 配置 `props`,无需改库表结构。
|
||||||
|
|
||||||
|
## 首页编辑(导航 / 下载 / 平台链接)
|
||||||
|
|
||||||
|
- **管理后台 → 首页编辑与下载**:导航链接、下载按钮链接、各平台链接均可点 **「选择链接」**,与积木编辑器共用 `LinkPickerDialog`。
|
||||||
|
- **本站页面**:来自当前站点「网页管理」中的页面路径(含首页 `/`)。
|
||||||
|
- **其他站点首页**:同账号下其他站点;需在 **站点管理** 中填写 **域名**,系统会生成 `https://域名/` 形式的外链。
|
||||||
|
- **可下载文件**:与积木一致,填入 `/api/web/sites/{siteId}/assets/{id}/download`。
|
||||||
|
- **试跳**:保存前可在后台用 **「试跳」** 新标签页预览;以 `/` 开头的路径会按当前浏览器域名拼接(与前台实际域名不一致时请以真实站点为准)。
|
||||||
|
|||||||
Reference in New Issue
Block a user