feat: FluTour增加指示器和动画效果

This commit is contained in:
Polaris-Night 2025-05-17 08:25:17 +08:00
parent e27f1591b0
commit 97e88dbd6f
6 changed files with 449 additions and 93 deletions

View File

@ -2737,49 +2737,60 @@ Some contents...</source>
<name>T_Tour</name>
<message>
<location filename="qml/page/T_Tour.qml" line="15"/>
<location filename="qml/page/T_Tour.qml" line="23"/>
<source>Upload File</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="15"/>
<location filename="qml/page/T_Tour.qml" line="23"/>
<source>Put your files here.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="16"/>
<location filename="qml/page/T_Tour.qml" line="52"/>
<location filename="qml/page/T_Tour.qml" line="54"/>
<location filename="qml/page/T_Tour.qml" line="24"/>
<location filename="qml/page/T_Tour.qml" line="74"/>
<location filename="qml/page/T_Tour.qml" line="76"/>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="16"/>
<location filename="qml/page/T_Tour.qml" line="24"/>
<source>Save your changes.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="17"/>
<location filename="qml/page/T_Tour.qml" line="25"/>
<source>Other Actions</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="17"/>
<location filename="qml/page/T_Tour.qml" line="25"/>
<source>Click to see other actions.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="31"/>
<location filename="qml/page/T_Tour.qml" line="46"/>
<source>Begin Tour</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="45"/>
<location filename="qml/page/T_Tour.qml" line="47"/>
<location filename="qml/page/T_Tour.qml" line="52"/>
<source>Begin Tour with custom indicator</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="67"/>
<location filename="qml/page/T_Tour.qml" line="69"/>
<source>Upload</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="61"/>
<location filename="qml/page/T_Tour.qml" line="83"/>
<source>More</source>
<translation type="unfinished"></translation>
</message>

View File

@ -552,7 +552,7 @@
<message>
<location filename="qml/global/ItemsOriginal.qml" line="448"/>
<source>Tour</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/global/ItemsOriginal.qml" line="454"/>
@ -2938,56 +2938,67 @@ Some contents...</source>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="15"/>
<location filename="qml/page/T_Tour.qml" line="23"/>
<source>Upload File</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="15"/>
<location filename="qml/page/T_Tour.qml" line="23"/>
<source>Put your files here.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="16"/>
<location filename="qml/page/T_Tour.qml" line="52"/>
<location filename="qml/page/T_Tour.qml" line="54"/>
<location filename="qml/page/T_Tour.qml" line="24"/>
<location filename="qml/page/T_Tour.qml" line="74"/>
<location filename="qml/page/T_Tour.qml" line="76"/>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="16"/>
<location filename="qml/page/T_Tour.qml" line="24"/>
<source>Save your changes.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="17"/>
<location filename="qml/page/T_Tour.qml" line="25"/>
<source>Other Actions</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="17"/>
<location filename="qml/page/T_Tour.qml" line="25"/>
<source>Click to see other actions.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="31"/>
<location filename="qml/page/T_Tour.qml" line="46"/>
<source>Begin Tour</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="45"/>
<location filename="qml/page/T_Tour.qml" line="47"/>
<location filename="qml/page/T_Tour.qml" line="52"/>
<source>Begin Tour with custom indicator</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="67"/>
<location filename="qml/page/T_Tour.qml" line="69"/>
<source>Upload</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="61"/>
<location filename="qml/page/T_Tour.qml" line="83"/>
<source>More</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Tour.qml" line="10"/>
<source>Tour</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
</context>
<context>

View File

@ -17,20 +17,42 @@ FluScrollablePage{
{title:qsTr("Other Actions"),description: qsTr("Click to see other actions."),target:()=>btn_more}
]
}
FluTour{
id:tour_custom_indicator
steps:[
{title:qsTr("Upload File"),description: qsTr("Put your files here."),target:()=>btn_upload},
{title:qsTr("Save"),description: qsTr("Save your changes."),target:()=>btn_save},
{title:qsTr("Other Actions"),description: qsTr("Click to see other actions."),target:()=>btn_more}
]
indicator: Component{
FluText {
text: "%1 / %2".arg(current + 1).arg(total)
}
}
}
FluFrame{
Layout.fillWidth: true
Layout.preferredHeight: 130
padding: 10
FluFilledButton{
Row{
anchors{
top: parent.top
topMargin: 14
}
text: qsTr("Begin Tour")
onClicked: {
tour.open()
spacing: 20
FluFilledButton{
text: qsTr("Begin Tour")
onClicked: {
tour.open()
}
}
FluFilledButton{
text: qsTr("Begin Tour with custom indicator")
onClicked: {
tour_custom_indicator.open()
}
}
}

View File

@ -7,8 +7,10 @@ import FluentUI 1.0
Popup{
property var steps : []
property int targetMargins: 5
property int targetRadius: 2
property Component nextButton: com_next_button
property Component prevButton: com_prev_button
property Component indicator: com_indicator
property int index : 0
property string finishText: qsTr("Finish")
property string nextText: qsTr("Next")
@ -22,12 +24,12 @@ Popup{
contentItem: Item{}
onVisibleChanged: {
if(visible){
d.animationEnabled = false
control.index = 0
d.updatePos()
d.animationEnabled = true
}
}
onIndexChanged: {
canvas.requestPaint()
}
Component{
id: com_next_button
FluFilledButton{
@ -50,10 +52,32 @@ Popup{
}
}
}
Component{
id: com_indicator
Row{
spacing: 10
Repeater{
model: total
delegate: Rectangle{
width: 8
height: 8
radius: 4
scale: current === index ? 1.2 : 1
color:{
if(current === index){
return FluTheme.primaryColor
}
return FluTheme.dark ? Qt.rgba(99/255,99/255,99/255,1) : Qt.rgba(214/255,214/255,214/255,1)
}
}
}
}
}
Item{
id:d
property var window: Window.window
property point pos: Qt.point(0,0)
property bool animationEnabled: true
property var step: steps[index]
property var target: {
if(steps[index]){
@ -73,15 +97,22 @@ Popup{
}
return control.width
}
function updatePos(){
if(d.target && d.window){
d.pos = d.target.mapToGlobal(0,0)
d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
}
}
onTargetChanged: {
updatePos()
}
}
Connections{
target: d.window
function onWidthChanged(){
canvas.requestPaint()
timer_delay.restart()
}
function onHeightChanged(){
canvas.requestPaint()
timer_delay.restart()
}
}
@ -89,39 +120,128 @@ Popup{
id: timer_delay
interval: 200
onTriggered: {
canvas.requestPaint()
d.updatePos()
}
}
Canvas{
id: canvas
anchors.fill: parent
onPaint: {
d.pos = d.target.mapToGlobal(0,0)
d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
var ctx = canvas.getContext("2d")
ctx.clearRect(0, 0, canvasSize.width, canvasSize.height)
ctx.save()
ctx.fillStyle = "#88000000"
ctx.fillRect(0, 0, canvasSize.width, canvasSize.height)
ctx.globalCompositeOperation = 'destination-out'
ctx.fillStyle = 'black'
var rect = Qt.rect(d.pos.x-control.targetMargins,d.pos.y-control.targetMargins, d.target.width+control.targetMargins*2, d.target.height+control.targetMargins*2)
drawRoundedRect(rect,2,ctx)
ctx.restore()
Item{
id: targetRect
x: d.pos.x - control.targetMargins
y: d.pos.y - control.targetMargins
width: d.target ? d.target.width + control.targetMargins * 2 : 0
height: d.target ? d.target.height + control.targetMargins * 2 : 0
Behavior on x {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
function drawRoundedRect(rect, r, ctx) {
ctx.beginPath();
ctx.moveTo(rect.x + r, rect.y);
ctx.lineTo(rect.x + rect.width - r, rect.y);
ctx.arcTo(rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + r, r);
ctx.lineTo(rect.x + rect.width, rect.y + rect.height - r);
ctx.arcTo(rect.x + rect.width, rect.y + rect.height, rect.x + rect.width - r, rect.y + rect.height, r);
ctx.lineTo(rect.x + r, rect.y + rect.height);
ctx.arcTo(rect.x, rect.y + rect.height, rect.x, rect.y + rect.height - r, r);
ctx.lineTo(rect.x, rect.y + r);
ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r);
ctx.closePath();
ctx.fill()
Behavior on y {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on width {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on height {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
}
Shape {
anchors.fill: parent
layer.enabled: true
layer.samples: 4
layer.smooth: true
ShapePath {
fillColor: "#88000000"
strokeWidth: 0
strokeColor: "transparent"
// draw background
PathMove {
x: 0
y: 0
}
PathLine {
x: control.width
y: 0
}
PathLine {
x: control.width
y: control.height
}
PathLine {
x: 0
y: control.height
}
PathLine {
x: 0
y: 0
}
// draw highlight
PathMove {
x: targetRect.x + control.targetRadius
y: targetRect.y
}
PathLine {
x: targetRect.x + targetRect.width - control.targetRadius
y: targetRect.y
}
PathArc {
x: targetRect.x + targetRect.width
y: targetRect.y + control.targetRadius
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
PathLine {
x: targetRect.x + targetRect.width
y: targetRect.y + targetRect.height - control.targetRadius
}
PathArc {
x: targetRect.x + targetRect.width - control.targetRadius
y: targetRect.y + targetRect.height
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
PathLine {
x: targetRect.x + control.targetRadius
y: targetRect.y + targetRect.height
}
PathArc {
x: targetRect.x
y: targetRect.y + targetRect.height - control.targetRadius
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
PathLine {
x: targetRect.x
y: targetRect.y + control.targetRadius
}
PathArc {
x: targetRect.x + control.targetRadius
y: targetRect.y
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
}
}
FluFrame{
@ -151,6 +271,18 @@ Popup{
return 0
}
border.width: 0
Behavior on x {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on y {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
FluShadow{
radius: 5
}
@ -193,10 +325,21 @@ Popup{
leftMargin: 15
}
}
FluLoader{
readonly property int total: steps.length
readonly property int current: control.index
sourceComponent: control.indicator
anchors{
bottom: parent.bottom
left: parent.left
bottomMargin: 15
leftMargin: 15
}
}
FluLoader{
id: loader_next
property bool isEnd: control.index === steps.length-1
sourceComponent: com_next_button
sourceComponent: control.nextButton
anchors{
top: text_desc.bottom
topMargin: 10
@ -207,7 +350,7 @@ Popup{
FluLoader{
id: loader_prev
visible: control.index !== 0
sourceComponent: com_prev_button
sourceComponent: control.prevButton
anchors{
right: loader_next.left
top: loader_next.top
@ -246,5 +389,17 @@ Popup{
}
return 0
}
Behavior on x {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on y {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
}
}

View File

@ -4328,8 +4328,10 @@ Module {
defaultProperty: "contentData"
Property { name: "steps"; type: "QVariant" }
Property { name: "targetMargins"; type: "int" }
Property { name: "targetRadius"; type: "int" }
Property { name: "nextButton"; type: "QQmlComponent"; isPointer: true }
Property { name: "prevButton"; type: "QQmlComponent"; isPointer: true }
Property { name: "indicator"; type: "QQmlComponent"; isPointer: true }
Property { name: "index"; type: "int" }
Property { name: "finishText"; type: "string" }
Property { name: "nextText"; type: "string" }

View File

@ -7,8 +7,10 @@ import FluentUI
Popup{
property var steps : []
property int targetMargins: 5
property int targetRadius: 2
property Component nextButton: com_next_button
property Component prevButton: com_prev_button
property Component indicator: com_indicator
property int index : 0
property string finishText: qsTr("Finish")
property string nextText: qsTr("Next")
@ -22,12 +24,12 @@ Popup{
contentItem: Item{}
onVisibleChanged: {
if(visible){
d.animationEnabled = false
control.index = 0
d.updatePos()
d.animationEnabled = true
}
}
onIndexChanged: {
canvas.requestPaint()
}
Component{
id: com_next_button
FluFilledButton{
@ -50,10 +52,32 @@ Popup{
}
}
}
Component{
id: com_indicator
Row{
spacing: 10
Repeater{
model: total
delegate: Rectangle{
width: 8
height: 8
radius: 4
scale: current === index ? 1.2 : 1
color:{
if(current === index){
return FluTheme.primaryColor
}
return FluTheme.dark ? Qt.rgba(99/255,99/255,99/255,1) : Qt.rgba(214/255,214/255,214/255,1)
}
}
}
}
}
Item{
id:d
property var window: Window.window
property point pos: Qt.point(0,0)
property bool animationEnabled: true
property var step: steps[index]
property var target: {
if(steps[index]){
@ -73,15 +97,22 @@ Popup{
}
return control.width
}
function updatePos(){
if(d.target && d.window){
d.pos = d.target.mapToGlobal(0,0)
d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
}
}
onTargetChanged: {
updatePos()
}
}
Connections{
target: d.window
function onWidthChanged(){
canvas.requestPaint()
timer_delay.restart()
}
function onHeightChanged(){
canvas.requestPaint()
timer_delay.restart()
}
}
@ -89,39 +120,128 @@ Popup{
id: timer_delay
interval: 200
onTriggered: {
canvas.requestPaint()
d.updatePos()
}
}
Canvas{
id: canvas
anchors.fill: parent
onPaint: {
d.pos = d.target.mapToGlobal(0,0)
d.pos = Qt.point(d.pos.x-d.window.x,d.pos.y-d.window.y)
var ctx = canvas.getContext("2d")
ctx.clearRect(0, 0, canvasSize.width, canvasSize.height)
ctx.save()
ctx.fillStyle = "#88000000"
ctx.fillRect(0, 0, canvasSize.width, canvasSize.height)
ctx.globalCompositeOperation = 'destination-out'
ctx.fillStyle = 'black'
var rect = Qt.rect(d.pos.x-control.targetMargins,d.pos.y-control.targetMargins, d.target.width+control.targetMargins*2, d.target.height+control.targetMargins*2)
drawRoundedRect(rect,2,ctx)
ctx.restore()
Item{
id: targetRect
x: d.pos.x - control.targetMargins
y: d.pos.y - control.targetMargins
width: d.target ? d.target.width + control.targetMargins * 2 : 0
height: d.target ? d.target.height + control.targetMargins * 2 : 0
Behavior on x {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
function drawRoundedRect(rect, r, ctx) {
ctx.beginPath();
ctx.moveTo(rect.x + r, rect.y);
ctx.lineTo(rect.x + rect.width - r, rect.y);
ctx.arcTo(rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + r, r);
ctx.lineTo(rect.x + rect.width, rect.y + rect.height - r);
ctx.arcTo(rect.x + rect.width, rect.y + rect.height, rect.x + rect.width - r, rect.y + rect.height, r);
ctx.lineTo(rect.x + r, rect.y + rect.height);
ctx.arcTo(rect.x, rect.y + rect.height, rect.x, rect.y + rect.height - r, r);
ctx.lineTo(rect.x, rect.y + r);
ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r);
ctx.closePath();
ctx.fill()
Behavior on y {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on width {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on height {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
}
Shape {
anchors.fill: parent
layer.enabled: true
layer.samples: 4
layer.smooth: true
ShapePath {
fillColor: "#88000000"
strokeWidth: 0
strokeColor: "transparent"
// draw background
PathMove {
x: 0
y: 0
}
PathLine {
x: control.width
y: 0
}
PathLine {
x: control.width
y: control.height
}
PathLine {
x: 0
y: control.height
}
PathLine {
x: 0
y: 0
}
// draw highlight
PathMove {
x: targetRect.x + control.targetRadius
y: targetRect.y
}
PathLine {
x: targetRect.x + targetRect.width - control.targetRadius
y: targetRect.y
}
PathArc {
x: targetRect.x + targetRect.width
y: targetRect.y + control.targetRadius
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
PathLine {
x: targetRect.x + targetRect.width
y: targetRect.y + targetRect.height - control.targetRadius
}
PathArc {
x: targetRect.x + targetRect.width - control.targetRadius
y: targetRect.y + targetRect.height
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
PathLine {
x: targetRect.x + control.targetRadius
y: targetRect.y + targetRect.height
}
PathArc {
x: targetRect.x
y: targetRect.y + targetRect.height - control.targetRadius
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
PathLine {
x: targetRect.x
y: targetRect.y + control.targetRadius
}
PathArc {
x: targetRect.x + control.targetRadius
y: targetRect.y
radiusX: control.targetRadius
radiusY: control.targetRadius
useLargeArc: false
direction: PathArc.Clockwise
}
}
}
FluFrame{
@ -151,6 +271,18 @@ Popup{
return 0
}
border.width: 0
Behavior on x {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on y {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
FluShadow{
radius: 5
}
@ -193,10 +325,21 @@ Popup{
leftMargin: 15
}
}
FluLoader{
readonly property int total: steps.length
readonly property int current: control.index
sourceComponent: control.indicator
anchors{
bottom: parent.bottom
left: parent.left
bottomMargin: 15
leftMargin: 15
}
}
FluLoader{
id: loader_next
property bool isEnd: control.index === steps.length-1
sourceComponent: com_next_button
sourceComponent: control.nextButton
anchors{
top: text_desc.bottom
topMargin: 10
@ -207,7 +350,7 @@ Popup{
FluLoader{
id: loader_prev
visible: control.index !== 0
sourceComponent: com_prev_button
sourceComponent: control.prevButton
anchors{
right: loader_next.left
top: loader_next.top
@ -246,5 +389,17 @@ Popup{
}
return 0
}
Behavior on x {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
Behavior on y {
enabled: d.animationEnabled && FluTheme.animationEnabled
NumberAnimation {
duration: 167
}
}
}
}