master
parent
3c562ae1ef
commit
a093799b01
@ -0,0 +1,26 @@
|
||||
# 在 server { } 内、location / 之前或之后按需 include。
|
||||
# root 须指向 Laravel 的 public 目录。
|
||||
|
||||
# 后台静态资源
|
||||
location ^~ /admin/assets/ {
|
||||
try_files $uri =404;
|
||||
access_log off;
|
||||
expires 7d;
|
||||
}
|
||||
|
||||
# 后台入口:仅 /admin 与 /admin/ 返回 index.html
|
||||
location = /admin {
|
||||
return 302 /admin/;
|
||||
}
|
||||
|
||||
location ^~ /admin/ {
|
||||
try_files $uri $uri/ @admin_spa;
|
||||
}
|
||||
|
||||
location @admin_spa {
|
||||
# 深链 /admin/courses 等 → Hash 路由(与 AdminSpaController 一致)
|
||||
if ($uri ~ "^/admin/(.+)$") {
|
||||
return 302 /admin/#/$1$is_args$args;
|
||||
}
|
||||
try_files /admin/index.html =404;
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { QuillEditor } from '@vueup/vue-quill'
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { uploadEditorImage, type EditorImageScope } from '@/api/admin/upload'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
height?: number
|
||||
scope?: EditorImageScope
|
||||
}>(),
|
||||
{
|
||||
height: 260,
|
||||
scope: 'common',
|
||||
},
|
||||
)
|
||||
|
||||
const content = defineModel<string>({ default: '' })
|
||||
|
||||
const toolbar = [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ header: [2, 3, false] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['link', 'image'],
|
||||
['clean'],
|
||||
]
|
||||
|
||||
type QuillInstance = {
|
||||
getSelection: (focus?: boolean) => { index: number; length: number } | null
|
||||
getLength: () => number
|
||||
insertEmbed: (index: number, type: string, value: string) => void
|
||||
setSelection: (index: number, length?: number) => void
|
||||
getModule: (name: string) => { addHandler: (name: string, handler: () => void) => void }
|
||||
}
|
||||
|
||||
function pickImage(quill: QuillInstance) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
try {
|
||||
const res = await uploadEditorImage(file, props.scope)
|
||||
if (!res.url) {
|
||||
ElMessage.error('图片上传失败')
|
||||
return
|
||||
}
|
||||
const range = quill.getSelection(true)
|
||||
const index = range?.index ?? Math.max(0, quill.getLength() - 1)
|
||||
quill.insertEmbed(index, 'image', res.url)
|
||||
quill.setSelection(index + 1)
|
||||
} catch (e: unknown) {
|
||||
const msg =
|
||||
(e as { response?: { data?: { message?: string } } })?.response?.data?.message ||
|
||||
'图片上传失败'
|
||||
ElMessage.error(typeof msg === 'string' ? msg : '图片上传失败')
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
function onEditorReady(quill: QuillInstance) {
|
||||
quill.getModule('toolbar').addHandler('image', () => pickImage(quill))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rich-text-editor" :style="{ height: `${height}px` }">
|
||||
<QuillEditor
|
||||
v-model:content="content"
|
||||
content-type="html"
|
||||
theme="snow"
|
||||
:toolbar="toolbar"
|
||||
@ready="onEditorReady"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rich-text-editor {
|
||||
width: 100%;
|
||||
}
|
||||
.rich-text-editor :deep(.ql-toolbar),
|
||||
.rich-text-editor :deep(.ql-container) {
|
||||
width: 100%;
|
||||
}
|
||||
.rich-text-editor :deep(.ql-container) {
|
||||
height: calc(100% - 42px);
|
||||
}
|
||||
.rich-text-editor :deep(.ql-editor img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,24 @@
|
||||
const isAdminDeploy = import.meta.env.MODE === 'admin'
|
||||
|
||||
function adminBaseUrl(): string {
|
||||
return `${window.location.origin}${import.meta.env.BASE_URL}`.replace(/\/?$/, '/')
|
||||
}
|
||||
|
||||
export function loginPathname(): string {
|
||||
return new URL('login', adminBaseUrl()).pathname
|
||||
}
|
||||
|
||||
export function loginHref(): string {
|
||||
if (isAdminDeploy) {
|
||||
return `${adminBaseUrl()}#/login`
|
||||
}
|
||||
return new URL('login', adminBaseUrl()).href
|
||||
}
|
||||
|
||||
export function isOnLoginPage(): boolean {
|
||||
if (isAdminDeploy) {
|
||||
const hash = window.location.hash.replace(/^#/, '')
|
||||
return hash === '/login' || hash.startsWith('/login?')
|
||||
}
|
||||
return window.location.pathname.startsWith(loginPathname())
|
||||
}
|
||||
Loading…
Reference in new issue