素材图片在高分辨率屏幕上清晰显示;项目资源对话框点整行勾选
- 素材库每张图另外渲染一份两倍尺寸(name.2x.png),build-gallery.sh 把图片编号(原尺寸 PNG 的 SHA-1,和运行器给图片的编号相同)和两倍图写进 index.json - 网页认出素材图片(按编号或素材网址),图片框用 srcset、背景用 image-set,系统缩放 150%、200% 的屏幕上不再发虚;学生程序读到的仍是原尺寸图片,宽高、评分都不变 - 素材清单 index.json 每次向服务器确认(不再缓存一小时),部署新素材后立即生效 - 项目资源对话框:点整行就能勾选(原来只能点复选框,"试听"不会勾选);缩略图用灰色棋盘格底, 透明背景上的白色图片(ico_title)也看得见 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f304fc1ffc
commit
c217eb5ca0
33 files changed
+98
-41
No files matched your search
@@ -45,13 +45,20 @@ function save() {
|
||||
<NModal :show="show" preset="card" title="项目资源" style="width: 560px; max-width: calc(100vw - 32px)" @update:show="emit('update:show', $event)">
|
||||
<p class="note">勾选要加入项目的图片和声音(平台素材库,名字和教材一致)。代码里写 <code>Properties.Resources.名字</code>,图片框的"图片"属性也可以直接选。</p>
|
||||
<div class="items">
|
||||
<label v-for="item in gallery" :key="item.name" class="item" :class="{ on: chosen.has(item.name) }">
|
||||
<NCheckbox :checked="chosen.has(item.name)" @update:checked="toggle(item.name, $event)" />
|
||||
<img v-if="item.kind === 'image'" :src="`/resources/${item.file}`" alt="" class="thumb" />
|
||||
<NButton v-else size="tiny" class="audio" @click.prevent="listen(item.file)">▶ 试听</NButton>
|
||||
<!-- 点整行都能勾选;复选框和"试听"自己处理点击,不再传给这一行 -->
|
||||
<div v-for="item in gallery" :key="item.name" class="item" :class="{ on: chosen.has(item.name) }" @click="toggle(item.name, !chosen.has(item.name))">
|
||||
<NCheckbox :checked="chosen.has(item.name)" @click.stop @update:checked="toggle(item.name, $event)" />
|
||||
<img
|
||||
v-if="item.kind === 'image'"
|
||||
:src="`/resources/${item.file}`"
|
||||
:srcset="item.hidpi ? `/resources/${item.file} 1x, /resources/${item.hidpi} 2x` : undefined"
|
||||
alt=""
|
||||
class="thumb"
|
||||
/>
|
||||
<NButton v-else size="tiny" class="audio" @click.stop="listen(item.file)">▶ 试听</NButton>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="gallery.length === 0" class="note">正在加载素材库…</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -106,6 +113,11 @@ function save() {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
/* 灰色棋盘格底:透明背景上的白色图片(如 ico_title 的白字)也看得见 */
|
||||
.thumb {
|
||||
background: repeating-conic-gradient(#9ca3af 0 25%, #c4c9d0 0 50%) 0 0 / 10px 10px;
|
||||
}
|
||||
|
||||
.audio {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface GalleryItem {
|
||||
/** 中文说明 */
|
||||
label: string
|
||||
kind: 'image' | 'audio'
|
||||
/** 图片的编号(原尺寸 PNG 的 SHA-1,和运行器给图片的编号相同) */
|
||||
key?: string
|
||||
/** 两倍尺寸的图片文件,高分辨率屏幕上显示用 */
|
||||
hidpi?: string
|
||||
}
|
||||
|
||||
export const RESOURCES_FILE = 'Properties/Resources.Designer.cs'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 运行区:学生程序的窗体(在一块"桌面"上)、提示框、虚拟实训台,以及编译错误和控制台输出。
|
||||
// 实验页和老师的评阅页共用。
|
||||
import { computed, onBeforeUnmount, onMounted, provide, ref } from 'vue'
|
||||
import { loadGallery } from '../designer/resources'
|
||||
import TrainingBoard from '../scene/TrainingBoard.vue'
|
||||
import type { SizeValue } from '../winforms/format'
|
||||
import FormWindow, { WINDOW_MARGIN, WINDOW_STEP } from '../winforms/FormWindow.vue'
|
||||
@@ -14,6 +15,8 @@ const emit = defineEmits<{ diagnostic: [file: string, line: number, column: numb
|
||||
|
||||
const state = props.session.state
|
||||
provide(RUN_IMAGES, props.session.images)
|
||||
// 素材库清单:认出素材图片,在高分辨率屏幕上换成两倍图
|
||||
void loadGallery()
|
||||
|
||||
/** 照片列表里点开看大图 */
|
||||
const viewing = ref<{ path: string; url: string } | null>(null)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { computed, inject, provide, ref, type CSSProperties } from 'vue'
|
||||
import type { ControlNode, UserAction } from '@iot/shared'
|
||||
import { AMBIENT_BACK_COLOR, AMBIENT_BACK_TYPES, boxCss, DEFAULT_BACK_COLOR, displayText } from './format'
|
||||
import { backgroundCss, RUN_IMAGES, type ImageValue } from './images'
|
||||
import { backgroundCss, imageSources, RUN_IMAGES, srcset, type ImageValue } from './images'
|
||||
import ChartControl from './ChartControl.vue'
|
||||
import DataGridViewControl from './DataGridViewControl.vue'
|
||||
import DateTimePickerControl from './DateTimePickerControl.vue'
|
||||
@@ -72,7 +72,7 @@ function selectItem(index: number) {
|
||||
|
||||
// PictureBox:按 SizeMode 摆放图片
|
||||
const image = computed(() => p.value.Image as ImageValue | null | undefined)
|
||||
const imageUrl = computed(() => (image.value ? image.value.url ?? runImages.get(image.value.key) : undefined))
|
||||
const imageSrc = computed(() => imageSources(image.value, runImages))
|
||||
// 设计器预览里不知道素材的尺寸,原样大小显示时让浏览器按图片本身的大小
|
||||
const naturalSize = computed(() => (image.value?.width ? { width: `${image.value.width}px`, height: `${image.value.height}px` } : {}))
|
||||
const imageStyle = computed((): CSSProperties => {
|
||||
@@ -182,7 +182,7 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
||||
@pointerdown="mouse('mouseDown', $event)"
|
||||
@pointerup="mouse('mouseUp', $event)"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" :style="imageStyle" alt="" draggable="false" />
|
||||
<img v-if="imageSrc" :src="imageSrc.src" :srcset="srcset(imageSrc)" :style="imageStyle" alt="" draggable="false" />
|
||||
<ControlView v-for="child in childrenTopLast" :key="child.id" :node="child" :act="props.act" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 运行时图片:运行器发来的图片数据由 RunSession 保存成 blob 网址,控件属性里只带图片编号。
|
||||
// 运行区 provide,ControlView inject;设计器预览没有运行时图片,取到空表。
|
||||
import type { CSSProperties, InjectionKey } from 'vue'
|
||||
import { gallery } from '../designer/resources'
|
||||
|
||||
export const RUN_IMAGES: InjectionKey<ReadonlyMap<string, string>> = Symbol('runImages')
|
||||
|
||||
@@ -13,12 +14,31 @@ export interface ImageValue {
|
||||
url?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片的网址。素材库里的图片(按编号或素材网址认出来)另有两倍尺寸的版本,
|
||||
* 高分辨率屏幕(系统缩放 150%、200%)上用它,不然浏览器放大原图会发虚;程序读到的宽高不变。
|
||||
*/
|
||||
export function imageSources(value: unknown, images: ReadonlyMap<string, string>): { src: string; hidpi?: string } | undefined {
|
||||
const image = value as ImageValue | null | undefined
|
||||
if (!image) return undefined
|
||||
const item = gallery.value.find((g) => (image.key && g.key === image.key) || (image.url && image.url === `/resources/${g.file}`))
|
||||
if (item?.hidpi) return { src: `/resources/${item.file}`, hidpi: `/resources/${item.hidpi}` }
|
||||
const src = image.url ?? images.get(image.key)
|
||||
return src ? { src } : undefined
|
||||
}
|
||||
|
||||
/** <img> 的 srcset:有两倍图时浏览器按屏幕分辨率选 */
|
||||
export const srcset = (sources: { src: string; hidpi?: string } | undefined) =>
|
||||
sources?.hidpi ? `${sources.src} 1x, ${sources.hidpi} 2x` : undefined
|
||||
|
||||
/** 背景图片的 CSS:BackgroundImageLayout 为 Tile(默认,平铺)、None(左上角一张)、Center、Stretch、Zoom */
|
||||
export function backgroundCss(value: unknown, layout: unknown, images: ReadonlyMap<string, string>): CSSProperties {
|
||||
const image = value as ImageValue | null | undefined
|
||||
const url = image ? image.url ?? images.get(image.key) : undefined
|
||||
if (!url) return {}
|
||||
const base = { backgroundImage: `url("${url}")` }
|
||||
const sources = imageSources(value, images)
|
||||
if (!sources) return {}
|
||||
// image-set 里的两倍图按 2x 显示,平铺、居中、左上角时仍是原尺寸
|
||||
const base: CSSProperties = {
|
||||
backgroundImage: sources.hidpi ? `image-set(url("${sources.src}") 1x, url("${sources.hidpi}") 2x)` : `url("${sources.src}")`,
|
||||
}
|
||||
switch (layout) {
|
||||
case 'None':
|
||||
return { ...base, backgroundRepeat: 'no-repeat', backgroundPosition: '0 0' }
|
||||
|
||||
Reference in new issue
Block a user