diff --git a/deploy/nginx-admin-spa.conf b/deploy/nginx-admin-spa.conf new file mode 100644 index 0000000..cb86b30 --- /dev/null +++ b/deploy/nginx-admin-spa.conf @@ -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; +} diff --git a/package.json b/package.json index 3e6aae3..e8e7acd 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "vue-tsc -b && vite build", - "build:admin": "vue-tsc -b && vite build --mode admin && cp deploy/admin.htaccess ../slake-school-service/public/admin/.htaccess", + "build:admin": "vue-tsc -b && vite build --mode admin && cp deploy/admin.htaccess ../slake-school-service/public/admin/.htaccess && cp deploy/nginx-admin-spa.conf ../slake-school-service/deploy/nginx-admin-spa.conf", "preview": "vite preview" }, "dependencies": { diff --git a/src/api/admin/upload.ts b/src/api/admin/upload.ts index c9c0345..c206773 100644 --- a/src/api/admin/upload.ts +++ b/src/api/admin/upload.ts @@ -4,6 +4,8 @@ import type { CourseMediaDto } from '@/api/admin/courses' export type CourseAssetSubdir = 'covers' | 'promos' +export type EditorImageScope = 'courses' | 'activities' | 'news' | 'banners' | 'common' + export async function uploadCourseAsset(file: File, subdir: CourseAssetSubdir) { const body = new FormData() body.append('file', file) @@ -25,3 +27,11 @@ export async function uploadBannerCover(file: File) { const { data } = await http.post>('/admin/v1/uploads/banner-cover', body) return data.data } + +export async function uploadEditorImage(file: File, scope: EditorImageScope = 'common') { + const body = new FormData() + body.append('file', file) + body.append('scope', scope) + const { data } = await http.post>('/admin/v1/uploads/editor-image', body) + return data.data +} diff --git a/src/components/CourseSigninQrDialog.vue b/src/components/CourseSigninQrDialog.vue index 4166b32..0d8654e 100644 --- a/src/components/CourseSigninQrDialog.vue +++ b/src/components/CourseSigninQrDialog.vue @@ -6,6 +6,7 @@ import { downloadSigninCard, getSigninCardFilename, openSigninCardPrintWindow, + renderSigninQrToCanvas, } from '@/utils/course-signin-qr' const props = withDefaults( @@ -28,37 +29,63 @@ const QR_SIZE = 200 const qrImageUrl = ref('') const qrReady = ref(false) const qrError = ref('') +const qrFallback = ref(false) const qrImageEl = ref(null) function revokeQrUrl() { - if (qrImageUrl.value) { + if (qrImageUrl.value.startsWith('blob:')) { URL.revokeObjectURL(qrImageUrl.value) - qrImageUrl.value = '' } + qrImageUrl.value = '' +} + +async function renderLocalQr() { + const canvas = document.createElement('canvas') + await renderSigninQrToCanvas(canvas, props.signinCode, QR_SIZE) + qrImageUrl.value = canvas.toDataURL('image/png') + qrReady.value = true + qrFallback.value = true } async function loadQr() { revokeQrUrl() qrReady.value = false qrError.value = '' + qrFallback.value = false if (!props.modelValue || !props.signinCode) return if (props.signinCode.length > 32) { - qrError.value = '签到码超过 32 字符,无法生成微信小程序码' + try { + await renderLocalQr() + } catch { + qrError.value = '签到码超过 32 字符,无法生成二维码' + } return } try { const blob = await fetchSigninQrImage(props.signinCode) if (!blob.type.includes('png') && blob.size < 4096) { - qrError.value = await readBlobErrorMessage(blob) + const message = await readBlobErrorMessage(blob) + try { + await renderLocalQr() + qrError.value = `${message},已改用普通二维码` + } catch { + qrError.value = message + } return } qrImageUrl.value = URL.createObjectURL(blob) qrReady.value = true } catch (error) { - qrError.value = error instanceof Error ? error.message : '生成小程序码失败' + const message = error instanceof Error ? error.message : '生成小程序码失败' + try { + await renderLocalQr() + qrError.value = `${message},已改用普通二维码` + } catch { + qrError.value = message + } } } @@ -112,10 +139,17 @@ function handlePrint() { alt="签到小程序码" />
暂无签到码
-
{{ qrError }}
+
{{ qrError }}
小程序码生成中…
+

{{ qrError }}

{{ signinCode }}

-

请使用微信扫一扫,将自动打开本小程序签到页

+

+ {{ + qrFallback + ? '当前为普通二维码,可在微信小程序内使用「扫一扫」扫描签到码文本' + : '请使用微信扫一扫,将自动打开本小程序签到页' + }} +