docs: 添加涉密文件自检工具实施计划

This commit is contained in:
2026-06-08 13:53:24 +08:00
commit 31161d9a5f
1838 changed files with 455407 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
// ==================================================
// =============== 可缩放的图片预览组件 ===============
// ==================================================
import QtQuick 2.15
import QtQuick.Controls 2.15
import ".."
Rectangle {
id: iRoot
// ========================= 【接口】 =========================
// 可设置
property real scaleMax: 2.0 // 比例上下限
property real scaleMin: 0.1 // 比例上下限
property QtObject overlayLayer // 图片叠加层
property var border: bRect.border // 边框
// 只读
property alias showImage: showImage // 图片组件
property real scale: 1.0 // 图片缩放比例
property int imageSW: 0 // 图片原始宽高
property int imageSH: 0
// 子类重写
property var beforeShow: undefined // 展示图片之前执行的操作
// 设置图片源,展示一张图片
function setSource(source) {
if(source) {
// 特殊字符#替换为%23
if(source.startsWith("file:///") && source.includes("#"))
source = source.replace(new RegExp("#", "g"), "%23");
showImage.source = source // 设置源
}
else
showImage.source = ""
}
// 传入路径,展示图片
function showPath(path) {
if(beforeShow) beforeShow()
showImage.showPath(path)
}
// 传入imgID展示图片
function showImgID(imgID) {
if(beforeShow) beforeShow()
showImage.showImgID(imgID)
}
// 清空展示
function clear() {
if(beforeShow) beforeShow()
// showImage.clear()
showImage.source = ""
imageSW = imageSH = 0
}
// 复制当前图片
function copyImage() {
if(showImage.source == "") return
const res = qmlapp.imageManager.copyImage(showImage.source)
if(res === "[Success]")
qmlapp.popup.simple(qsTr("复制图片"), "")
else
qmlapp.popup.simple(qsTr("复制图片失败"), res)
}
// 用系统默认应用打开图片
function openImage() {
if(showImage.source == "") return
const res = qmlapp.imageManager.openImage(showImage.source)
if(res === "[Success]")
qmlapp.popup.simple(qsTr("打开图片"), "")
else
qmlapp.popup.simple(qsTr("打开图片失败"), res)
}
// 保存当前图片
function saveImage() {
if(showImage.source == "") return
saveDialog.open()
}
FileDialog_ {
id: saveDialog
title: qsTr("保存图片")
selectExisting: false
selectFolder: false
folder: shortcuts.desktop // 默认放桌面
nameFilters: ["*.png", "*.jpg"]
onAccepted: {
if(!fileUrl) {
console.log("文件对话框:未选择任何文件")
return
}
let filePath = fileUrl
const res = qmlapp.imageManager.saveImage(showImage.source, filePath)
if(res.startsWith("[Success]"))
qmlapp.popup.simple(qsTr("保存图片"), res)
else
qmlapp.popup.simple(qsTr("保存图片失败"), res)
}
}
// ========================= 【处理】 =========================
Component.onCompleted: {
// 叠加层挂父级
if(overlayLayer && overlayLayer.hasOwnProperty("parent"))
overlayLayer.parent = showImage
}
// 图片组件的状态改变
function imageStatusChanged(s) {
// 已就绪
if(s == Image.Ready) {
imageSW = showImage.sourceSize.width // 记录图片原始宽高
imageSH = showImage.sourceSize.height
imageFullFit() // 初始大小
}
else {
imageSW = imageSH = 0
iRoot.scale = 1
}
}
// 缩放,传入 flag>0 放大, <0 缩小 0回归100%。以相框中心为锚点。
function imageScaleAddSub(flag=0, step=0.1) {
if(showImage.status != Image.Ready) return
// 计算缩放比例
let s = 1.0 // flag==0 时复原
if (flag > 0) { // 放大
s = (iRoot.scale + step).toFixed(1)
// 禁止大于上限 或 图片填满大小(裁切)
const max = Math.max(flickable.width/imageSW, flickable.height/imageSH, scaleMax)
if(s > max) s = max
}
else if(flag < 0) { // 缩小
s = (iRoot.scale - step).toFixed(1)
// 禁止小于下限 或 图片填满大小(不裁切)
const min = Math.min(flickable.width/imageSW, flickable.height/imageSH, scaleMin)
if(s < min) s = min
}
// 目标锚点
let gx = -flickable.width/2
let gy = -flickable.height/2
// 目标锚点在图片中的原比例
let s1x = (flickable.contentX-gx)/showImageContainer.width
let s1y = (flickable.contentY-gy)/showImageContainer.height
// 目标锚点在图片中的新比例,及差值
iRoot.scale = s // 更新缩放
let s2x = (flickable.contentX-gx)/showImageContainer.width
let s2y = (flickable.contentY-gy)/showImageContainer.height
let sx = s2x-s1x
let sy = s2y-s1y
// 实际长度差值
let lx = sx*showImageContainer.width
let ly = sy*showImageContainer.height
// 偏移
flickable.contentX -= lx
flickable.contentY -= ly
}
// 图片填满组件,不裁切
function imageFullFit() {
if(showImage.source == "" || imageSW <= 0) return
iRoot.scale = Math.min(flickable.width/imageSW, flickable.height/imageSH)
// 图片中心对齐相框
flickable.contentY = - (flickable.height - showImageContainer.height)/2
flickable.contentX = - (flickable.width - showImageContainer.width)/2
}
// ======================== 【布局】 =========================
color: theme.bgColor
// 滑动区域,显示图片,监听左键拖拽
Flickable {
id: flickable
anchors.fill: parent
contentWidth: showImageContainer.width
contentHeight: showImageContainer.height
clip: true
// 图片容器,大小不小于滑动区域
Item {
id: showImageContainer
width: Math.max( imageSW * iRoot.scale , flickable.width )
height: Math.max( imageSH * iRoot.scale , flickable.height )
Image_ {
id: showImage
anchors.centerIn: parent
scale: iRoot.scale
onStatusChanged: imageStatusChanged(status)
}
}
// 滚动条
ScrollBar.vertical: ScrollBar { }
ScrollBar.horizontal: ScrollBar { }
}
// 监听滚轮缩放
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
// 滚轮缩放
onWheel: {
if (wheel.angleDelta.y > 0) {
imageScaleAddSub(1) // 放大
}
else {
imageScaleAddSub(-1) // 缩小
}
}
}
// 边框
Rectangle {
id: bRect
anchors.fill: parent
color: "#00000000"
border.width: 1
border.color: theme.coverColor4
}
}

View File

@@ -0,0 +1,300 @@
// ==========================================================
// =============== 可显示OCR文本的增强Image组件 ===============
// ==========================================================
import QtQuick 2.15
import QtQuick.Controls 2.15
import ".."
ImageScale {
id: iRoot
property bool showOverlay: true // 显示叠加层
Component.onCompleted: {
// 默认显示/关闭叠加层
showOverlay = qmlapp.globalConfigs.getValue("ui.imgShowOverlay")
}
beforeShow: () => {
mouseArea.initIndex() // 清空选字参数
textBoxes = [] // 清空旧文本块
}
// 展示文本块
function showTextBoxes(res) {
beforeShow()
// 提取文本框
if(res.code == 100 && res.data.length > 0) {
let tbs = []
for(let i in res.data) {
const d = res.data[i]
const info = {
x: d.box[0][0],
y: d.box[0][1],
width: d.box[2][0] - d.box[0][0],
height: d.box[2][1] - d.box[0][1],
text: d.text,
end: d.end || "", // 行尾间隔符
}
tbs.push(info)
}
textBoxes = tbs
}
}
// 弹出菜单
function popupMenu() {
selectMenu.popup()
}
// 显示/隐藏叠加层
function switchOverlay() {
showOverlay = !showOverlay
if(!showOverlay)
mouseArea.cursorShape = Qt.OpenHandCursor
}
property var textBoxes: [] // 文本块列表
// 文本块叠加层
overlayLayer: Item {
id: oRoot
anchors.fill: parent
visible: showOverlay
Repeater {
id: textBoxRepeater
model: textBoxes
TextBox {
text: modelData.text
x: modelData.x
y: modelData.y
end: modelData.end // 结尾间隔符
Component.onCompleted: {
width = modelData.width
height = modelData.height
resetSize() // 自适应字体和组件大小
textBoxes[index].width = width // 记录修改后的组件大小
textBoxes[index].height = height
textBoxes[index].obj = this
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
property int startIndex: -1 // 拖拽开始时,文本框序号
property int startTextIndex: -1 // 拖拽开始时,字符序号
property int endIndex: -1 // 拖拽结束时,文本框序号
property int endTextIndex: -1 // 拖拽结束时,字符序号
property int selectUpdate: 0
acceptedButtons: Qt.LeftButton | Qt.RightButton
// 清除index
function initIndex() {
startIndex = startTextIndex = endIndex = endTextIndex = -1
}
// 检测当前鼠标点位于哪一个tb内
function mouseInTextBox() {
const localPoint = oRoot.mapFromItem(mouseArea, mouseX, mouseY)
const x = localPoint.x, y = localPoint.y
for (let i=0,l=textBoxes.length; i<l; i++) {
const rect = textBoxes[i]
if (x >= rect.x && x <= rect.x + rect.width &&
y >= rect.y && y <= rect.y + rect.height) {
return i
}
}
return -1
}
// 检测当前鼠标点在index tb内的哪一个字符处
function mouseInTextIndex(index) {
return textBoxes[index].obj.where(mouseArea, mouseX, mouseY)
}
// 获取Index正确顺序。返回 [li 起始块, lt 起始块选区左侧, ri 结束块, rt 结束块选区右侧]
function getIndexes() {
let li, lt, ri, rt
if(startIndex < endIndex) {
li=startIndex; lt=startTextIndex; ri=endIndex; rt=endTextIndex;
}
else if(startIndex > endIndex) {
li=endIndex; lt=endTextIndex; ri=startIndex; rt=startTextIndex;
}
else {
li = ri = startIndex
if(startTextIndex < endTextIndex) {
lt=startTextIndex; rt=endTextIndex;
}
else if(startTextIndex > endTextIndex) {
lt=endTextIndex; rt=startTextIndex;
}
else { // 单击,未选中
lt = rt = -1
}
}
return [li, lt, ri, rt]
}
// 根据 Index 的参数,选择对应文本。
function selectIndex() {
const [li, lt, ri, rt] = getIndexes()
// 遍历每个文本框数据
for (let i = 0, l=textBoxes.length; i < l; i++) {
const tEdit = textBoxes[i].obj.textEdit
if( li<0 || ri<0 || i<li || i>ri ) { // 未被选中
tEdit.deselect()
}
else if(i === li && i === ri) { // 单个块
if(lt === rt) // 无有效选中
tEdit.deselect()
else
tEdit.select(lt, rt)
}
else if(i === li) { // 多个块的起始
const len = textBoxes[i].text.length
tEdit.select(lt, len)
}
else if(i === ri) { // 多个块的结束
tEdit.select(0, rt)
}
else { // 多个块的中间
tEdit.selectAll(0, rt)
}
}
}
// 全选
function selectAll() {
const l = textBoxes.length
if(l === 0) return
startIndex = startTextIndex = 0
endIndex = l-1
endTextIndex = textBoxes[endIndex].text.length
selectIndex()
}
// 复制已选中的内容
function selectCopy() {
let [li, lt, ri, rt] = getIndexes()
// 没有有效选中,则复制全部
if(li<0 || ri<0 || (li===ri && lt===rt)) {
selectAllCopy()
return
}
let copyText = ""
// 选中单个文本块
if(li === ri) {
copyText = textBoxes[li].text.substring(lt, rt)
}
// 选中多个块,则遍历多个块,提取各自的文本
else {
for(let i = li; i <= ri; i++) {
const text = textBoxes[i].text
const end = textBoxes[i].end
if(i === li) // 多个块的起始
copyText = text.substring(lt) + end
else if(i === ri) // 多个块的结束
copyText += text.substring(0, rt)
else // 多个块的中间
copyText += text + end
}
}
if(copyText && copyText.length > 0) {
qmlapp.utilsConnector.copyText(copyText)
qmlapp.popup.simple(qsTr("图片:复制%1字").arg(copyText.length), "")
}
else {
qmlapp.popup.simple(qsTr("图片:无选中文字"), "")
}
}
// 复制所有
function selectAllCopy() {
let copyText = ""
for (let i = 0, l=textBoxes.length; i < l; i++) {
copyText += textBoxes[i].text
if(i < l-1) copyText += textBoxes[i].end
}
qmlapp.utilsConnector.copyText(copyText)
qmlapp.popup.simple(qsTr("图片:复制全部%1字").arg(copyText.length), "")
selectAll()
}
// 按下
onPressed: {
mouseArea.forceActiveFocus()
if (mouse.button === Qt.RightButton) {
selectMenu.popup()
return
}
if(!showOverlay) {
mouse.accepted = false
return
}
initIndex()
const tbi = mouseInTextBox()
cursorShape = tbi < 0 ? Qt.ClosedHandCursor : Qt.IBeamCursor
if(tbi >= 0) { // 选择文本
startIndex = tbi
startTextIndex = mouseInTextIndex(tbi)
}
else {
mouse.accepted = false
}
}
// 移动
onPositionChanged: {
if(!showOverlay) return
const tbi = mouseInTextBox()
if(pressed) { // 拖拽中
if(tbi >= 0) { // 选择文本
endIndex = tbi
endTextIndex = mouseInTextIndex(tbi)
selectIndex()
}
}
else { // 悬停中
cursorShape = tbi < 0 ? Qt.OpenHandCursor : Qt.IBeamCursor
}
}
// 抬起
onReleased: {
if(!showOverlay) return
if (mouse.button === Qt.RightButton) return
const tbi = mouseInTextBox()
cursorShape = tbi < 0 ? Qt.OpenHandCursor : Qt.IBeamCursor
if(startIndex === -1) return
if(tbi >= 0) {
endIndex = tbi
endTextIndex = mouseInTextIndex(tbi)
}
selectIndex()
}
// 菜单
Menu_ {
id: selectMenu
menuList: [
[mouseArea.selectCopy, qsTr("复制  Ctrl+C")],
[mouseArea.selectAll, qsTr("全选  Ctrl+A")],
[iRoot.copyImage, qsTr("复制图片Ctrl+X")],
[iRoot.saveImage, qsTr("保存图片Ctrl+S")],
[iRoot.switchOverlay, qsTr("显示/隐藏文字Tab")],
[iRoot.openImage, qsTr("用默认应用打开图片")],
[iRoot.clear, qsTr("删除图片Ctrl+D"), "noColor"],
]
}
// 按键事件
Keys.onPressed: {
if (event.modifiers & Qt.ControlModifier) {
event.key===Qt.Key_A && selectAll()
event.key===Qt.Key_C && selectCopy()
event.key===Qt.Key_X && iRoot.copyImage()
event.key===Qt.Key_S && iRoot.saveImage()
event.key===Qt.Key_D && iRoot.clear()
}
if (event.key === Qt.Key_Tab) {
iRoot.switchOverlay()
Qt.callLater(mouseArea.forceActiveFocus)
}
}
}
}

View File

@@ -0,0 +1,87 @@
// =============================================
// =============== 单个文本块组件 ===============
// =============================================
import QtQuick 2.15
import QtQuick.Controls 2.15
import ".."
Item {
id: tRoot
property string text: ""
// 颜色固定,不受主题影响
property color bgColor: "#99000000"
property color textColor: "#FFFFFF"
// 外部接口,重设字体和组件大小
property var resetSize: textEdit.resetFontSize
property alias textEdit: textEdit // 文本组件
property string end: "" // 结尾间隔符
// 传入一个相对于item的坐标返回该坐标的文本序号。
function where(item, mx, my) {
const textPoint = textEdit.mapFromItem(item, mx, my)
const textPos = textEdit.positionAt(textPoint.x, textPoint.y)
return textPos
}
// 背景
Rectangle {
id: bgRect
anchors.fill: parent
color: bgColor
}
// 结尾标识
Icon_ {
icon: "line_feed"
visible: end == "\n" // 标记换行符
anchors.left: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 1
height: Math.min(tRoot.height * 0.8 ,tRoot.width * 0.8 , size_.line * 2)
width: height
color: theme.subTextColor
}
// 文本
TextEdit_ {
id: textEdit
anchors.fill: parent
color: textColor
readOnly: true // 只读
selectByMouse: false // 禁止选择文本
selectByKeyboard: false
persistentSelection: true // 丢失焦点时,保留选区
font.pixelSize: 10 // 初始10像素
// 重设字体大小,以适合组件大小
function resetFontSize() {
text = tRoot.text
// 初次调整,利用初始文字面积与容器面积的比值,计算字体大小
let s = 1
if(contentWidth>0 && contentHeight>0)
s = (width * height) / (contentWidth * contentHeight)
let ps = font.pixelSize * Math.sqrt(s)
font.pixelSize = ps
// 二次调整:如果文本比容器高出至少半行,则减小字体大小,直到不高于容器
if(contentHeight >= height+(ps*0.5)) {
// 为了保持性能,限定调整的最大次数
for(let i=0; i<10 && contentHeight > height; i++) {
font.pixelSize--
}
}
// 二次调整:如果当前只有一行,则优化字间距
if(lineCount === 1 && text.length > 0) {
const s = (tRoot.width - contentWidth) / text.length
if(s > 0) {
font.letterSpacing = s
}
}
// 优化整体宽高
if(contentWidth > tRoot.width)
tRoot.width = contentWidth
if(contentHeight > tRoot.height)
tRoot.height = contentHeight
}
}
}