mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-30 20:50:52 +08:00
🎨 Style(custom): lint code
This commit is contained in:
@@ -1,115 +1,107 @@
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-placeholder"
|
||||
>
|
||||
<div class="loading-spinner" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="
|
||||
isShowThumbnail && item.isImage
|
||||
? base64Image
|
||||
: `./assets/icons/${getFileIconPath(item.fileName ?? '')}`
|
||||
"
|
||||
alt=""
|
||||
class="image"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
:src="`./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
|
||||
alt=""
|
||||
class="image"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
|
||||
import { getFileIconPath } from '@/manage/utils/common'
|
||||
|
||||
const base64Image = ref('')
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
isShowThumbnail: boolean
|
||||
item: {
|
||||
isImage: boolean
|
||||
fileName: string
|
||||
}
|
||||
localPath: string
|
||||
}>()
|
||||
|
||||
const createBase64Image = async () => {
|
||||
try {
|
||||
const filePath = window.node.path.normalize(props.localPath)
|
||||
const base64 = await window.node.fs.readFile(filePath, 'base64')
|
||||
base64Image.value = `data:${window.node.mime.lookup(filePath) || 'image/png'};base64,${base64}`
|
||||
isLoading.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
hasError.value = true
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = true
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await createBase64Image()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div v-if="isLoading" class="loading-placeholder">
|
||||
<div class="loading-spinner" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="isShowThumbnail && item.isImage ? base64Image : `./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
|
||||
alt=""
|
||||
class="image"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img v-else :src="`./assets/icons/${getFileIconPath(item.fileName ?? '')}`" alt="" class="image" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
|
||||
import { getFileIconPath } from '@/manage/utils/common'
|
||||
|
||||
const base64Image = ref('')
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
isShowThumbnail: boolean
|
||||
item: {
|
||||
isImage: boolean
|
||||
fileName: string
|
||||
}
|
||||
localPath: string
|
||||
}>()
|
||||
|
||||
const createBase64Image = async () => {
|
||||
try {
|
||||
const filePath = window.node.path.normalize(props.localPath)
|
||||
const base64 = await window.node.fs.readFile(filePath, 'base64')
|
||||
base64Image.value = `data:${window.node.mime.lookup(filePath) || 'image/png'};base64,${base64}`
|
||||
isLoading.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
hasError.value = true
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = true
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await createBase64Image()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,123 +1,123 @@
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-placeholder"
|
||||
>
|
||||
<div class="loading-spinner" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="imageSource"
|
||||
alt=""
|
||||
class="image"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
:src="iconPath"
|
||||
alt=""
|
||||
class="image"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { getFileIconPath } from '@/manage/utils/common'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
|
||||
const preSignedUrl = ref('')
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
key: string
|
||||
isImage: boolean
|
||||
fileName: string | null | undefined
|
||||
}
|
||||
alias: string
|
||||
url: string
|
||||
config: any
|
||||
isShowThumbnail: boolean
|
||||
}>()
|
||||
|
||||
const imageSource = computed(() => {
|
||||
return props.isShowThumbnail && props.item.isImage
|
||||
? preSignedUrl.value
|
||||
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
|
||||
})
|
||||
|
||||
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
|
||||
|
||||
async function getUrl () {
|
||||
try {
|
||||
isLoading.value = true
|
||||
hasError.value = false
|
||||
preSignedUrl.value = await window.electron.triggerRPC<any>(IRPCActionType.MANAGE_GET_PRE_SIGNED_URL, props.alias, props.config)
|
||||
isLoading.value = false
|
||||
} catch (error) {
|
||||
console.error('Failed to get pre-signed URL:', error)
|
||||
hasError.value = true
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = true
|
||||
}
|
||||
|
||||
watch(() => [props.url, props.item], getUrl, { deep: true })
|
||||
|
||||
onMounted(getUrl)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div v-if="isLoading" class="loading-placeholder">
|
||||
<div class="loading-spinner" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="imageSource"
|
||||
alt=""
|
||||
class="image"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img v-else :src="iconPath" alt="" class="image" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { getFileIconPath } from '@/manage/utils/common'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
|
||||
const preSignedUrl = ref('')
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
key: string
|
||||
isImage: boolean
|
||||
fileName: string | null | undefined
|
||||
}
|
||||
alias: string
|
||||
url: string
|
||||
config: any
|
||||
isShowThumbnail: boolean
|
||||
}>()
|
||||
|
||||
const imageSource = computed(() => {
|
||||
return props.isShowThumbnail && props.item.isImage
|
||||
? preSignedUrl.value
|
||||
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
|
||||
})
|
||||
|
||||
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
|
||||
|
||||
async function getUrl() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
hasError.value = false
|
||||
preSignedUrl.value = await window.electron.triggerRPC<any>(
|
||||
IRPCActionType.MANAGE_GET_PRE_SIGNED_URL,
|
||||
props.alias,
|
||||
props.config
|
||||
)
|
||||
isLoading.value = false
|
||||
} catch (error) {
|
||||
console.error('Failed to get pre-signed URL:', error)
|
||||
hasError.value = true
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = true
|
||||
}
|
||||
|
||||
watch(() => [props.url, props.item], getUrl, { deep: true })
|
||||
|
||||
onMounted(getUrl)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,154 +1,150 @@
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-placeholder"
|
||||
>
|
||||
<div class="loading-spinner" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="imageSource"
|
||||
alt=""
|
||||
class="image"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
:src="iconPath"
|
||||
alt=""
|
||||
class="image"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { getFileIconPath } from '@/manage/utils/common'
|
||||
import { getAuthHeader } from '@/manage/utils/digestAuth'
|
||||
import { formatEndpoint } from '@/utils/common'
|
||||
|
||||
const base64Url = ref('')
|
||||
const success = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
key: string
|
||||
isImage: boolean
|
||||
fileName: string | null | undefined
|
||||
}
|
||||
url: string
|
||||
config: any
|
||||
isShowThumbnail: boolean
|
||||
}>()
|
||||
|
||||
const imageSource = computed(() => {
|
||||
return props.isShowThumbnail && props.item.isImage && success.value
|
||||
? base64Url.value
|
||||
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
|
||||
})
|
||||
|
||||
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
|
||||
|
||||
async function getWebdavHeader (key: string) {
|
||||
let headers = {} as any
|
||||
if (props.config.authType === 'digest') {
|
||||
const authHeader = await getAuthHeader(
|
||||
'GET',
|
||||
formatEndpoint(props.config.endpoint, props.config.sslEnabled || false),
|
||||
`/${key.replace(/^\//, '')}`,
|
||||
props.config.username,
|
||||
props.config.password
|
||||
)
|
||||
headers = {
|
||||
Authorization: authHeader
|
||||
}
|
||||
} else {
|
||||
headers = {
|
||||
Authorization: 'Basic ' + Buffer.from(`${props.config.username}:${props.config.password}`).toString('base64')
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
const fetchImage = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
hasError.value = false
|
||||
const headers = await getWebdavHeader(props.item.key)
|
||||
const res = await fetch(props.url, { method: 'GET', headers })
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
const blob = await res.blob()
|
||||
success.value = true
|
||||
base64Url.value = URL.createObjectURL(blob)
|
||||
isLoading.value = false
|
||||
} else {
|
||||
throw new Error('Network response was not ok.')
|
||||
}
|
||||
} catch (err) {
|
||||
success.value = false
|
||||
hasError.value = true
|
||||
isLoading.value = false
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = true
|
||||
}
|
||||
|
||||
watch(() => [props.url, props.item], fetchImage, { deep: true })
|
||||
|
||||
onMounted(fetchImage)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div v-if="isLoading" class="loading-placeholder">
|
||||
<div class="loading-spinner" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="imageSource"
|
||||
alt=""
|
||||
class="image"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img v-else :src="iconPath" alt="" class="image" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { getFileIconPath } from '@/manage/utils/common'
|
||||
import { getAuthHeader } from '@/manage/utils/digestAuth'
|
||||
import { formatEndpoint } from '@/utils/common'
|
||||
|
||||
const base64Url = ref('')
|
||||
const success = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
key: string
|
||||
isImage: boolean
|
||||
fileName: string | null | undefined
|
||||
}
|
||||
url: string
|
||||
config: any
|
||||
isShowThumbnail: boolean
|
||||
}>()
|
||||
|
||||
const imageSource = computed(() => {
|
||||
return props.isShowThumbnail && props.item.isImage && success.value
|
||||
? base64Url.value
|
||||
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
|
||||
})
|
||||
|
||||
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
|
||||
|
||||
async function getWebdavHeader(key: string) {
|
||||
let headers = {} as any
|
||||
if (props.config.authType === 'digest') {
|
||||
const authHeader = await getAuthHeader(
|
||||
'GET',
|
||||
formatEndpoint(props.config.endpoint, props.config.sslEnabled || false),
|
||||
`/${key.replace(/^\//, '')}`,
|
||||
props.config.username,
|
||||
props.config.password
|
||||
)
|
||||
headers = {
|
||||
Authorization: authHeader
|
||||
}
|
||||
} else {
|
||||
headers = {
|
||||
Authorization: 'Basic ' + Buffer.from(`${props.config.username}:${props.config.password}`).toString('base64')
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
const fetchImage = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
hasError.value = false
|
||||
const headers = await getWebdavHeader(props.item.key)
|
||||
const res = await fetch(props.url, { method: 'GET', headers })
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
const blob = await res.blob()
|
||||
success.value = true
|
||||
base64Url.value = URL.createObjectURL(blob)
|
||||
isLoading.value = false
|
||||
} else {
|
||||
throw new Error('Network response was not ok.')
|
||||
}
|
||||
} catch (err) {
|
||||
success.value = false
|
||||
hasError.value = true
|
||||
isLoading.value = false
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = true
|
||||
}
|
||||
|
||||
watch(() => [props.url, props.item], fetchImage, { deep: true })
|
||||
|
||||
onMounted(fetchImage)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showInputBoxVisible"
|
||||
class="inputbox-overlay"
|
||||
@click="handleInputBoxCancel"
|
||||
>
|
||||
<div
|
||||
class="inputbox-container"
|
||||
@click.stop
|
||||
>
|
||||
<div v-if="showInputBoxVisible" class="inputbox-overlay" @click="handleInputBoxCancel">
|
||||
<div class="inputbox-container" @click.stop>
|
||||
<div class="inputbox-header">
|
||||
<h3 class="inputbox-title">
|
||||
{{ inputBoxOptions.title || t('pages.inputBox.title') }}
|
||||
</h3>
|
||||
<button
|
||||
class="inputbox-close"
|
||||
@click="handleInputBoxCancel"
|
||||
>
|
||||
<button class="inputbox-close" @click="handleInputBoxCancel">
|
||||
<X :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -28,19 +18,13 @@
|
||||
type="text"
|
||||
@keyup.enter="handleInputBoxConfirm"
|
||||
@keyup.escape="handleInputBoxCancel"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="inputbox-actions">
|
||||
<button
|
||||
class="inputbox-btn cancel-btn"
|
||||
@click="handleInputBoxCancel"
|
||||
>
|
||||
<button class="inputbox-btn cancel-btn" @click="handleInputBoxCancel">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="inputbox-btn confirm-btn primary"
|
||||
@click="handleInputBoxConfirm"
|
||||
>
|
||||
<button class="inputbox-btn confirm-btn primary" @click="handleInputBoxConfirm">
|
||||
{{ t('common.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -66,27 +50,27 @@ const inputBoxOptions = reactive({
|
||||
placeholder: ''
|
||||
})
|
||||
|
||||
let removeInputBoxListenerCallback: (() => void) = () => {}
|
||||
let removeInputBoxListenerCallback: () => void = () => {}
|
||||
|
||||
function handleIpcInputBoxEvent (options: IShowInputBoxOption) {
|
||||
function handleIpcInputBoxEvent(options: IShowInputBoxOption) {
|
||||
initInputBoxValue(options)
|
||||
}
|
||||
|
||||
function initInputBoxValue (options: IShowInputBoxOption) {
|
||||
function initInputBoxValue(options: IShowInputBoxOption) {
|
||||
inputBoxValue.value = options.value || ''
|
||||
inputBoxOptions.title = options.title || ''
|
||||
inputBoxOptions.placeholder = options.placeholder || ''
|
||||
showInputBoxVisible.value = true
|
||||
}
|
||||
|
||||
function handleInputBoxCancel () {
|
||||
function handleInputBoxCancel() {
|
||||
// TODO: RPCServer
|
||||
showInputBoxVisible.value = false
|
||||
window.electron.sendToMain(SHOW_INPUT_BOX, '')
|
||||
$bus.emit(SHOW_INPUT_BOX_RESPONSE, '')
|
||||
}
|
||||
|
||||
function handleInputBoxConfirm () {
|
||||
function handleInputBoxConfirm() {
|
||||
showInputBoxVisible.value = false
|
||||
window.electron.sendToMain(SHOW_INPUT_BOX, inputBoxValue.value)
|
||||
$bus.emit(SHOW_INPUT_BOX_RESPONSE, inputBoxValue.value)
|
||||
@@ -126,7 +110,9 @@ export default {
|
||||
.inputbox-container {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 32rem;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,61 +1,57 @@
|
||||
<template>
|
||||
<div class="toolbox-handler">
|
||||
<button
|
||||
class="handler-button"
|
||||
@click="() => props.handler(value)"
|
||||
>
|
||||
{{ props.handlerText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
interface IProps {
|
||||
status: string
|
||||
value: any
|
||||
handlerText: string
|
||||
handler: (value: any) => void | Promise<void>
|
||||
}
|
||||
|
||||
const props = defineProps<IProps>()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ToolboxHandler'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.toolbox-handler {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.handler-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
font-family: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.handler-button:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.handler-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="toolbox-handler">
|
||||
<button class="handler-button" @click="() => props.handler(value)">
|
||||
{{ props.handlerText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface IProps {
|
||||
status: string
|
||||
value: any
|
||||
handlerText: string
|
||||
handler: (value: any) => void | Promise<void>
|
||||
}
|
||||
|
||||
const props = defineProps<IProps>()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ToolboxHandler'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.toolbox-handler {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.handler-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
font-family: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.handler-button:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.handler-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<component
|
||||
:is="icon"
|
||||
class="toolbox-status-icon"
|
||||
:style="{ color }"
|
||||
/>
|
||||
<component :is="icon" class="toolbox-status-icon" :style="{ color }" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="virtual-scroller"
|
||||
:style="{ height: `${containerHeight}px` }"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div
|
||||
class="virtual-scroller-content"
|
||||
:style="contentStyles"
|
||||
>
|
||||
<div ref="containerRef" class="virtual-scroller" :style="{ height: `${containerHeight}px` }" @scroll="handleScroll">
|
||||
<div class="virtual-scroller-content" :style="contentStyles">
|
||||
<div
|
||||
class="virtual-scroller-viewport"
|
||||
:class="{ 'is-grid': isGridMode, 'is-list': !isGridMode }"
|
||||
@@ -16,14 +8,15 @@
|
||||
>
|
||||
<div
|
||||
v-for="realIndex in visibleIndexes"
|
||||
:key="itemsRef[realIndex] && itemsRef[realIndex][props.keyField || 'id'] ? itemsRef[realIndex][props.keyField || 'id'] : realIndex"
|
||||
:key="
|
||||
itemsRef[realIndex] && itemsRef[realIndex][props.keyField || 'id']
|
||||
? itemsRef[realIndex][props.keyField || 'id']
|
||||
: realIndex
|
||||
"
|
||||
class="virtual-scroller-item"
|
||||
:style="itemStyle"
|
||||
>
|
||||
<slot
|
||||
:item="itemsRef[realIndex]"
|
||||
:index="realIndex"
|
||||
/>
|
||||
<slot :item="itemsRef[realIndex]" :index="realIndex" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,29 +29,35 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useVirtualGrid } from '@/hooks/useVirtualGrid'
|
||||
|
||||
type Item = any
|
||||
interface Breakpoint { min: number; cols: number }
|
||||
interface Breakpoint {
|
||||
min: number
|
||||
cols: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
items: Item[]
|
||||
itemHeight: number
|
||||
height?: number
|
||||
gridItems?: number
|
||||
gridBreakpoints?: Breakpoint[]
|
||||
bufferFactor?: number
|
||||
pageMode?: boolean
|
||||
keyField?: string
|
||||
itemPadding?: number
|
||||
viewMode?: 'list' | 'grid'
|
||||
}>(), {
|
||||
height: 400,
|
||||
gridItems: 1,
|
||||
gridBreakpoints: () => [],
|
||||
bufferFactor: 0.5,
|
||||
pageMode: false,
|
||||
keyField: 'id',
|
||||
itemPadding: 0,
|
||||
viewMode: 'grid'
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: Item[]
|
||||
itemHeight: number
|
||||
height?: number
|
||||
gridItems?: number
|
||||
gridBreakpoints?: Breakpoint[]
|
||||
bufferFactor?: number
|
||||
pageMode?: boolean
|
||||
keyField?: string
|
||||
itemPadding?: number
|
||||
viewMode?: 'list' | 'grid'
|
||||
}>(),
|
||||
{
|
||||
height: 400,
|
||||
gridItems: 1,
|
||||
gridBreakpoints: () => [],
|
||||
bufferFactor: 0.5,
|
||||
pageMode: false,
|
||||
keyField: 'id',
|
||||
itemPadding: 0,
|
||||
viewMode: 'grid'
|
||||
}
|
||||
)
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const containerHeight = ref<number>(props.pageMode ? 0 : props.height)
|
||||
@@ -66,15 +65,23 @@ const containerWidth = ref<number>(0)
|
||||
const parentScrollListeners = ref<HTMLElement[]>([])
|
||||
|
||||
const itemsRef = ref<Item[]>(props.items)
|
||||
watch(() => props.items, v => { itemsRef.value = v })
|
||||
|
||||
const localViewMode = ref< 'list' | 'grid'>(props.viewMode)
|
||||
watch(() => props.viewMode, v => { localViewMode.value = v })
|
||||
|
||||
const sortedBreakpoints = computed<Breakpoint[]>(() =>
|
||||
[...props.gridBreakpoints].sort((a, b) => a.min - b.min)
|
||||
watch(
|
||||
() => props.items,
|
||||
v => {
|
||||
itemsRef.value = v
|
||||
}
|
||||
)
|
||||
|
||||
const localViewMode = ref<'list' | 'grid'>(props.viewMode)
|
||||
watch(
|
||||
() => props.viewMode,
|
||||
v => {
|
||||
localViewMode.value = v
|
||||
}
|
||||
)
|
||||
|
||||
const sortedBreakpoints = computed<Breakpoint[]>(() => [...props.gridBreakpoints].sort((a, b) => a.min - b.min))
|
||||
|
||||
const isForcedList = computed(() => localViewMode.value === 'list')
|
||||
|
||||
const effectiveCols = computed<number>(() => {
|
||||
@@ -92,19 +99,14 @@ const effectiveCols = computed<number>(() => {
|
||||
|
||||
const isGridMode = computed(() => effectiveCols.value > 1)
|
||||
|
||||
const {
|
||||
gridCalculations,
|
||||
visibleIndexes,
|
||||
viewportOffset,
|
||||
updateScrollTop,
|
||||
scrollToItem, scrollToTop, scrollToBottom
|
||||
} = useVirtualGrid({
|
||||
items: itemsRef,
|
||||
itemHeight: props.itemHeight,
|
||||
containerHeight,
|
||||
gridItems: effectiveCols,
|
||||
bufferFactor: props.bufferFactor
|
||||
})
|
||||
const { gridCalculations, visibleIndexes, viewportOffset, updateScrollTop, scrollToItem, scrollToTop, scrollToBottom } =
|
||||
useVirtualGrid({
|
||||
items: itemsRef,
|
||||
itemHeight: props.itemHeight,
|
||||
containerHeight,
|
||||
gridItems: effectiveCols,
|
||||
bufferFactor: props.bufferFactor
|
||||
})
|
||||
|
||||
const contentStyles = computed(() => ({
|
||||
height: `${gridCalculations.value.totalHeight}px`
|
||||
@@ -122,19 +124,15 @@ const viewportStyle = computed(() => {
|
||||
return base
|
||||
})
|
||||
|
||||
const itemStyle = computed(() =>
|
||||
isGridMode.value
|
||||
? {}
|
||||
: { height: `${props.itemHeight}px` }
|
||||
)
|
||||
const itemStyle = computed(() => (isGridMode.value ? {} : { height: `${props.itemHeight}px` }))
|
||||
|
||||
function handleScroll () {
|
||||
function handleScroll() {
|
||||
const c = containerRef.value
|
||||
if (!c) return
|
||||
updateScrollTop(c.scrollTop)
|
||||
}
|
||||
|
||||
function handlePageScroll () {
|
||||
function handlePageScroll() {
|
||||
if (!props.pageMode) return
|
||||
const now = Date.now()
|
||||
if (now - lastScrollTime.value < 16) return
|
||||
@@ -159,7 +157,7 @@ function handlePageScroll () {
|
||||
let ro: ResizeObserver | null = null
|
||||
const lastScrollTime = ref(0)
|
||||
|
||||
function updateContainerMetrics () {
|
||||
function updateContainerMetrics() {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
@@ -206,16 +204,18 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function scrollTo (index: number) { scrollToItem(index) }
|
||||
function scrollTo(index: number) {
|
||||
scrollToItem(index)
|
||||
}
|
||||
|
||||
function setViewMode (mode: 'list' | 'grid') {
|
||||
function setViewMode(mode: 'list' | 'grid') {
|
||||
localViewMode.value = mode
|
||||
}
|
||||
function toggleViewMode () {
|
||||
function toggleViewMode() {
|
||||
setViewMode(isGridMode.value ? 'list' : 'grid')
|
||||
}
|
||||
|
||||
function refresh () {
|
||||
function refresh() {
|
||||
updateContainerMetrics()
|
||||
if (containerRef.value) {
|
||||
updateScrollTop(containerRef.value.scrollTop)
|
||||
@@ -266,5 +266,4 @@ defineExpose({ scrollTo, scrollToTop, scrollToBottom, setViewMode, toggleViewMod
|
||||
.virtual-scroller-viewport.is-grid .virtual-scroller-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,342 +1,305 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="messagebox-overlay"
|
||||
@click="onCancel"
|
||||
>
|
||||
<div
|
||||
class="messagebox-container"
|
||||
@click.stop
|
||||
>
|
||||
<div class="messagebox-header">
|
||||
<h3 class="messagebox-title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<button
|
||||
v-if="showClose"
|
||||
class="messagebox-close"
|
||||
@click="onCancel"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="messagebox-content">
|
||||
<div
|
||||
v-if="type"
|
||||
class="messagebox-icon"
|
||||
>
|
||||
<component
|
||||
:is="iconComponent"
|
||||
:size="48"
|
||||
/>
|
||||
</div>
|
||||
<div class="messagebox-message">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="center"
|
||||
class="messagebox-actions center"
|
||||
>
|
||||
<button
|
||||
class="messagebox-btn cancel-btn"
|
||||
@click="onCancel"
|
||||
>
|
||||
{{ cancelButtonText }}
|
||||
</button>
|
||||
<button
|
||||
class="messagebox-btn confirm-btn"
|
||||
:class="confirmButtonClass"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ confirmButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="messagebox-actions"
|
||||
>
|
||||
<button
|
||||
class="messagebox-btn confirm-btn"
|
||||
:class="confirmButtonClass"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ confirmButtonText }}
|
||||
</button>
|
||||
<button
|
||||
class="messagebox-btn cancel-btn"
|
||||
@click="onCancel"
|
||||
>
|
||||
{{ cancelButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AlertTriangle, CheckCircle, Info, XCircle } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
title?: string
|
||||
message: string
|
||||
type?: 'info' | 'success' | 'warning' | 'error'
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
showClose?: boolean
|
||||
center?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: 'Confirm',
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
showClose: true,
|
||||
center: false,
|
||||
type: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'warning':
|
||||
return AlertTriangle
|
||||
case 'info':
|
||||
return Info
|
||||
case 'success':
|
||||
return CheckCircle
|
||||
case 'error':
|
||||
return XCircle
|
||||
default:
|
||||
return Info
|
||||
}
|
||||
})
|
||||
|
||||
const confirmButtonClass = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'warning':
|
||||
case 'error':
|
||||
return 'danger'
|
||||
case 'success':
|
||||
return 'success'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
})
|
||||
|
||||
const onConfirm = () => {
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ConfirmMessageBox'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.messagebox-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.messagebox-container {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 32rem;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root.dark .messagebox-container,
|
||||
:root.auto.dark .messagebox-container {
|
||||
background: rgb(31 41 55);
|
||||
border: 1px solid rgb(55 65 81);
|
||||
}
|
||||
|
||||
.messagebox-header {
|
||||
padding: 1.5rem 1.5rem 0 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.messagebox-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgb(17 24 39);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:root.dark .messagebox-title,
|
||||
:root.auto.dark .messagebox-title {
|
||||
color: rgb(243 244 246);
|
||||
}
|
||||
|
||||
.messagebox-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: rgb(107 114 128);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.messagebox-close:hover {
|
||||
background: rgb(243 244 246);
|
||||
color: rgb(17 24 39);
|
||||
}
|
||||
|
||||
:root.dark .messagebox-close,
|
||||
:root.auto.dark .messagebox-close {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
:root.dark .messagebox-close:hover,
|
||||
:root.auto.dark .messagebox-close:hover {
|
||||
background: rgb(55 65 81);
|
||||
color: rgb(243 244 246);
|
||||
}
|
||||
|
||||
.messagebox-content {
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.messagebox-icon {
|
||||
flex-shrink: 0;
|
||||
color: rgb(107 114 128);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide="alert-triangle"] {
|
||||
color: rgb(245 158 11);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide="info"] {
|
||||
color: rgb(59 130 246);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide="check-circle"] {
|
||||
color: rgb(34 197 94);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide="x-circle"] {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.messagebox-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.messagebox-message p {
|
||||
color: rgb(107 114 128);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:root.dark .messagebox-message p,
|
||||
:root.auto.dark .messagebox-message p {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.messagebox-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.messagebox-actions.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.messagebox-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: rgb(243 244 246);
|
||||
color: rgb(75 85 99);
|
||||
border: 1px solid rgb(209 213 219);
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: rgb(229 231 235);
|
||||
}
|
||||
|
||||
:root.dark .cancel-btn,
|
||||
:root.auto.dark .cancel-btn {
|
||||
background: rgb(55 65 81);
|
||||
color: rgb(209 213 219);
|
||||
border-color: rgb(75 85 99);
|
||||
}
|
||||
|
||||
:root.dark .cancel-btn:hover,
|
||||
:root.auto.dark .cancel-btn:hover {
|
||||
background: rgb(75 85 99);
|
||||
}
|
||||
|
||||
.confirm-btn.primary {
|
||||
background: rgb(59 130 246);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn.primary:hover {
|
||||
background: rgb(37 99 235);
|
||||
}
|
||||
|
||||
.confirm-btn.danger {
|
||||
background: rgb(239 68 68);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn.danger:hover {
|
||||
background: rgb(220 38 38);
|
||||
}
|
||||
|
||||
.confirm-btn.success {
|
||||
background: rgb(34 197 94);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn.success:hover {
|
||||
background: rgb(22 163 74);
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div v-if="isOpen" class="messagebox-overlay" @click="onCancel">
|
||||
<div class="messagebox-container" @click.stop>
|
||||
<div class="messagebox-header">
|
||||
<h3 class="messagebox-title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<button v-if="showClose" class="messagebox-close" @click="onCancel">×</button>
|
||||
</div>
|
||||
<div class="messagebox-content">
|
||||
<div v-if="type" class="messagebox-icon">
|
||||
<component :is="iconComponent" :size="48" />
|
||||
</div>
|
||||
<div class="messagebox-message">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="center" class="messagebox-actions center">
|
||||
<button class="messagebox-btn cancel-btn" @click="onCancel">
|
||||
{{ cancelButtonText }}
|
||||
</button>
|
||||
<button class="messagebox-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
|
||||
{{ confirmButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="messagebox-actions">
|
||||
<button class="messagebox-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
|
||||
{{ confirmButtonText }}
|
||||
</button>
|
||||
<button class="messagebox-btn cancel-btn" @click="onCancel">
|
||||
{{ cancelButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AlertTriangle, CheckCircle, Info, XCircle } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
title?: string
|
||||
message: string
|
||||
type?: 'info' | 'success' | 'warning' | 'error'
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
showClose?: boolean
|
||||
center?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: 'Confirm',
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
showClose: true,
|
||||
center: false,
|
||||
type: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'warning':
|
||||
return AlertTriangle
|
||||
case 'info':
|
||||
return Info
|
||||
case 'success':
|
||||
return CheckCircle
|
||||
case 'error':
|
||||
return XCircle
|
||||
default:
|
||||
return Info
|
||||
}
|
||||
})
|
||||
|
||||
const confirmButtonClass = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'warning':
|
||||
case 'error':
|
||||
return 'danger'
|
||||
case 'success':
|
||||
return 'success'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
})
|
||||
|
||||
const onConfirm = () => {
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ConfirmMessageBox'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.messagebox-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.messagebox-container {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 32rem;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root.dark .messagebox-container,
|
||||
:root.auto.dark .messagebox-container {
|
||||
background: rgb(31 41 55);
|
||||
border: 1px solid rgb(55 65 81);
|
||||
}
|
||||
|
||||
.messagebox-header {
|
||||
padding: 1.5rem 1.5rem 0 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.messagebox-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgb(17 24 39);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:root.dark .messagebox-title,
|
||||
:root.auto.dark .messagebox-title {
|
||||
color: rgb(243 244 246);
|
||||
}
|
||||
|
||||
.messagebox-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: rgb(107 114 128);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.messagebox-close:hover {
|
||||
background: rgb(243 244 246);
|
||||
color: rgb(17 24 39);
|
||||
}
|
||||
|
||||
:root.dark .messagebox-close,
|
||||
:root.auto.dark .messagebox-close {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
:root.dark .messagebox-close:hover,
|
||||
:root.auto.dark .messagebox-close:hover {
|
||||
background: rgb(55 65 81);
|
||||
color: rgb(243 244 246);
|
||||
}
|
||||
|
||||
.messagebox-content {
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.messagebox-icon {
|
||||
flex-shrink: 0;
|
||||
color: rgb(107 114 128);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide='alert-triangle'] {
|
||||
color: rgb(245 158 11);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide='info'] {
|
||||
color: rgb(59 130 246);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide='check-circle'] {
|
||||
color: rgb(34 197 94);
|
||||
}
|
||||
|
||||
.messagebox-icon svg[data-lucide='x-circle'] {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.messagebox-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.messagebox-message p {
|
||||
color: rgb(107 114 128);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:root.dark .messagebox-message p,
|
||||
:root.auto.dark .messagebox-message p {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.messagebox-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.messagebox-actions.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.messagebox-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: rgb(243 244 246);
|
||||
color: rgb(75 85 99);
|
||||
border: 1px solid rgb(209 213 219);
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: rgb(229 231 235);
|
||||
}
|
||||
|
||||
:root.dark .cancel-btn,
|
||||
:root.auto.dark .cancel-btn {
|
||||
background: rgb(55 65 81);
|
||||
color: rgb(209 213 219);
|
||||
border-color: rgb(75 85 99);
|
||||
}
|
||||
|
||||
:root.dark .cancel-btn:hover,
|
||||
:root.auto.dark .cancel-btn:hover {
|
||||
background: rgb(75 85 99);
|
||||
}
|
||||
|
||||
.confirm-btn.primary {
|
||||
background: rgb(59 130 246);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn.primary:hover {
|
||||
background: rgb(37 99 235);
|
||||
}
|
||||
|
||||
.confirm-btn.danger {
|
||||
background: rgb(239 68 68);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn.danger:hover {
|
||||
background: rgb(220 38 38);
|
||||
}
|
||||
|
||||
.confirm-btn.success {
|
||||
background: rgb(34 197 94);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn.success:hover {
|
||||
background: rgb(22 163 74);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,270 +1,257 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="message-container">
|
||||
<TransitionGroup
|
||||
name="message"
|
||||
tag="div"
|
||||
>
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-toast"
|
||||
:class="getMessageClass(message.type)"
|
||||
>
|
||||
<div class="message-icon">
|
||||
<component
|
||||
:is="getIconComponent(message.type)"
|
||||
:size="20"
|
||||
/>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
{{ message.message }}
|
||||
</div>
|
||||
<button
|
||||
v-if="message.showClose"
|
||||
class="message-close"
|
||||
@click="removeMessage(message.id)"
|
||||
>
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AlertTriangle, CheckCircle, Info, X, XCircle } from 'lucide-vue-next'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export interface MessageOptions {
|
||||
message: string
|
||||
type?: 'success' | 'warning' | 'info' | 'error'
|
||||
duration?: number
|
||||
showClose?: boolean
|
||||
}
|
||||
|
||||
interface MessageItem extends MessageOptions {
|
||||
id: string
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
const messages = reactive<MessageItem[]>([])
|
||||
|
||||
const getIconComponent = (type: MessageOptions['type']) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return CheckCircle
|
||||
case 'warning':
|
||||
return AlertTriangle
|
||||
case 'error':
|
||||
return XCircle
|
||||
default:
|
||||
return Info
|
||||
}
|
||||
}
|
||||
|
||||
const getMessageClass = (type: MessageOptions['type']) => {
|
||||
return `message-${type || 'info'}`
|
||||
}
|
||||
|
||||
const removeMessage = (id: string) => {
|
||||
const index = messages.findIndex(msg => msg.id === id)
|
||||
if (index > -1) {
|
||||
const message = messages[index]
|
||||
if (message.timer) {
|
||||
clearTimeout(message.timer)
|
||||
}
|
||||
messages.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addMessage = (options: MessageOptions) => {
|
||||
const id = `message-${Date.now()}-${Math.random()}`
|
||||
const duration = options.duration ?? 3000
|
||||
const showClose = options.showClose ?? true
|
||||
|
||||
const message: MessageItem = {
|
||||
id,
|
||||
...options,
|
||||
showClose
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
message.timer = setTimeout(() => {
|
||||
removeMessage(id)
|
||||
}, duration)
|
||||
}
|
||||
|
||||
messages.push(message)
|
||||
return id
|
||||
}
|
||||
|
||||
// Expose methods for external use
|
||||
const success = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'success', ...options })
|
||||
}
|
||||
|
||||
const error = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'error', ...options })
|
||||
}
|
||||
|
||||
const warning = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'warning', ...options })
|
||||
}
|
||||
|
||||
const info = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'info', ...options })
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
addMessage,
|
||||
removeMessage
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MessageToast'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-container {
|
||||
position: fixed;
|
||||
top: 34px;
|
||||
right: 20px;
|
||||
z-index: 3000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.message-toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
max-width: 24rem;
|
||||
min-width: 20rem;
|
||||
pointer-events: all;
|
||||
background: white;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
:root.dark .message-toast,
|
||||
:root.auto.dark .message-toast {
|
||||
background: rgb(31 41 55);
|
||||
border-color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
.message-info {
|
||||
border-left: 4px solid rgb(59 130 246);
|
||||
}
|
||||
|
||||
.message-info .message-icon {
|
||||
color: rgb(59 130 246);
|
||||
}
|
||||
|
||||
.message-success {
|
||||
border-left: 4px solid rgb(34 197 94);
|
||||
}
|
||||
|
||||
.message-success .message-icon {
|
||||
color: rgb(34 197 94);
|
||||
}
|
||||
|
||||
.message-warning {
|
||||
border-left: 4px solid rgb(245 158 11);
|
||||
}
|
||||
|
||||
.message-warning .message-icon {
|
||||
color: rgb(245 158 11);
|
||||
}
|
||||
|
||||
.message-error {
|
||||
border-left: 4px solid rgb(239 68 68);
|
||||
}
|
||||
|
||||
.message-error .message-icon {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
color: rgb(75 85 99);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:root.dark .message-content,
|
||||
:root.auto.dark .message-content {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
.message-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgb(107 114 128);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.message-close:hover {
|
||||
background: rgb(243 244 246);
|
||||
color: rgb(75 85 99);
|
||||
}
|
||||
|
||||
:root.dark .message-close,
|
||||
:root.auto.dark .message-close {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
:root.dark .message-close:hover,
|
||||
:root.auto.dark .message-close:hover {
|
||||
background: rgb(55 65 81);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
/* Transition animations */
|
||||
.message-enter-active,
|
||||
.message-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.message-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="message-container">
|
||||
<TransitionGroup name="message" tag="div">
|
||||
<div v-for="message in messages" :key="message.id" class="message-toast" :class="getMessageClass(message.type)">
|
||||
<div class="message-icon">
|
||||
<component :is="getIconComponent(message.type)" :size="20" />
|
||||
</div>
|
||||
<div class="message-content">
|
||||
{{ message.message }}
|
||||
</div>
|
||||
<button v-if="message.showClose" class="message-close" @click="removeMessage(message.id)">
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AlertTriangle, CheckCircle, Info, X, XCircle } from 'lucide-vue-next'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export interface MessageOptions {
|
||||
message: string
|
||||
type?: 'success' | 'warning' | 'info' | 'error'
|
||||
duration?: number
|
||||
showClose?: boolean
|
||||
}
|
||||
|
||||
interface MessageItem extends MessageOptions {
|
||||
id: string
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
const messages = reactive<MessageItem[]>([])
|
||||
|
||||
const getIconComponent = (type: MessageOptions['type']) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return CheckCircle
|
||||
case 'warning':
|
||||
return AlertTriangle
|
||||
case 'error':
|
||||
return XCircle
|
||||
default:
|
||||
return Info
|
||||
}
|
||||
}
|
||||
|
||||
const getMessageClass = (type: MessageOptions['type']) => {
|
||||
return `message-${type || 'info'}`
|
||||
}
|
||||
|
||||
const removeMessage = (id: string) => {
|
||||
const index = messages.findIndex(msg => msg.id === id)
|
||||
if (index > -1) {
|
||||
const message = messages[index]
|
||||
if (message.timer) {
|
||||
clearTimeout(message.timer)
|
||||
}
|
||||
messages.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addMessage = (options: MessageOptions) => {
|
||||
const id = `message-${Date.now()}-${Math.random()}`
|
||||
const duration = options.duration ?? 3000
|
||||
const showClose = options.showClose ?? true
|
||||
|
||||
const message: MessageItem = {
|
||||
id,
|
||||
...options,
|
||||
showClose
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
message.timer = setTimeout(() => {
|
||||
removeMessage(id)
|
||||
}, duration)
|
||||
}
|
||||
|
||||
messages.push(message)
|
||||
return id
|
||||
}
|
||||
|
||||
// Expose methods for external use
|
||||
const success = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'success', ...options })
|
||||
}
|
||||
|
||||
const error = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'error', ...options })
|
||||
}
|
||||
|
||||
const warning = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'warning', ...options })
|
||||
}
|
||||
|
||||
const info = (message: string, options?: Partial<MessageOptions>) => {
|
||||
return addMessage({ message, type: 'info', ...options })
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
addMessage,
|
||||
removeMessage
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MessageToast'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-container {
|
||||
position: fixed;
|
||||
top: 34px;
|
||||
right: 20px;
|
||||
z-index: 3000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.message-toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
max-width: 24rem;
|
||||
min-width: 20rem;
|
||||
pointer-events: all;
|
||||
background: white;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
:root.dark .message-toast,
|
||||
:root.auto.dark .message-toast {
|
||||
background: rgb(31 41 55);
|
||||
border-color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
.message-info {
|
||||
border-left: 4px solid rgb(59 130 246);
|
||||
}
|
||||
|
||||
.message-info .message-icon {
|
||||
color: rgb(59 130 246);
|
||||
}
|
||||
|
||||
.message-success {
|
||||
border-left: 4px solid rgb(34 197 94);
|
||||
}
|
||||
|
||||
.message-success .message-icon {
|
||||
color: rgb(34 197 94);
|
||||
}
|
||||
|
||||
.message-warning {
|
||||
border-left: 4px solid rgb(245 158 11);
|
||||
}
|
||||
|
||||
.message-warning .message-icon {
|
||||
color: rgb(245 158 11);
|
||||
}
|
||||
|
||||
.message-error {
|
||||
border-left: 4px solid rgb(239 68 68);
|
||||
}
|
||||
|
||||
.message-error .message-icon {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
color: rgb(75 85 99);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:root.dark .message-content,
|
||||
:root.auto.dark .message-content {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
.message-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgb(107 114 128);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.message-close:hover {
|
||||
background: rgb(243 244 246);
|
||||
color: rgb(75 85 99);
|
||||
}
|
||||
|
||||
:root.dark .message-close,
|
||||
:root.auto.dark .message-close {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
:root.dark .message-close:hover,
|
||||
:root.auto.dark .message-close:hover {
|
||||
background: rgb(55 65 81);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
/* Transition animations */
|
||||
.message-enter-active,
|
||||
.message-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.message-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,117 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { Monitor, Moon, Sun } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useAppStore } from '@/hooks/useAppStore'
|
||||
|
||||
interface Props {
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const currentTheme = computed(() => appStore.settings.app.theme || 'light')
|
||||
|
||||
const themeOptions = computed(() => [
|
||||
{
|
||||
value: 'light',
|
||||
label: t('settings.theme.light'),
|
||||
icon: Sun,
|
||||
description: t('settings.theme.lightDesc')
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: t('settings.theme.dark'),
|
||||
icon: Moon,
|
||||
description: t('settings.theme.darkDesc')
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('settings.theme.auto'),
|
||||
icon: Monitor,
|
||||
description: t('settings.theme.autoDesc')
|
||||
}
|
||||
])
|
||||
|
||||
const currentThemeOption = computed(
|
||||
() => themeOptions.value.find(option => option.value === currentTheme.value) || themeOptions.value[0]
|
||||
)
|
||||
|
||||
const toggleTheme = () => {
|
||||
appStore.toggleTheme()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-switcher">
|
||||
<button
|
||||
class="theme-toggle-btn"
|
||||
:class="{ collapsed }"
|
||||
:title="t('settings.theme.toggle')"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<component
|
||||
:is="currentThemeOption.icon"
|
||||
:size="18"
|
||||
/>
|
||||
<span
|
||||
v-if="!collapsed"
|
||||
class="theme-label"
|
||||
>{{ currentThemeOption.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-switcher {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle-btn.collapsed {
|
||||
padding: 0.5rem;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
background: var(--color-surface-elevated);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.theme-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
padding: 0.5rem;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { Monitor, Moon, Sun } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useAppStore } from '@/hooks/useAppStore'
|
||||
|
||||
interface Props {
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const currentTheme = computed(() => appStore.settings.app.theme || 'light')
|
||||
|
||||
const themeOptions = computed(() => [
|
||||
{
|
||||
value: 'light',
|
||||
label: t('settings.theme.light'),
|
||||
icon: Sun,
|
||||
description: t('settings.theme.lightDesc')
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: t('settings.theme.dark'),
|
||||
icon: Moon,
|
||||
description: t('settings.theme.darkDesc')
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('settings.theme.auto'),
|
||||
icon: Monitor,
|
||||
description: t('settings.theme.autoDesc')
|
||||
}
|
||||
])
|
||||
|
||||
const currentThemeOption = computed(
|
||||
() => themeOptions.value.find(option => option.value === currentTheme.value) || themeOptions.value[0]
|
||||
)
|
||||
|
||||
const toggleTheme = () => {
|
||||
appStore.toggleTheme()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-switcher">
|
||||
<button class="theme-toggle-btn" :class="{ collapsed }" :title="t('settings.theme.toggle')" @click="toggleTheme">
|
||||
<component :is="currentThemeOption.icon" :size="18" />
|
||||
<span v-if="!collapsed" class="theme-label">{{ currentThemeOption.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-switcher {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle-btn.collapsed {
|
||||
padding: 0.5rem;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
background: var(--color-surface-elevated);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.theme-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
padding: 0.5rem;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,246 +1,212 @@
|
||||
<template>
|
||||
<div
|
||||
class="title-bar"
|
||||
data-drag-region
|
||||
>
|
||||
<div class="title-bar-content">
|
||||
<div
|
||||
v-if="osGlobal !== 'darwin'"
|
||||
class="title-left"
|
||||
>
|
||||
<div class="app-icon">
|
||||
<img
|
||||
:src="defaultLogo"
|
||||
alt="App Icon"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-center">
|
||||
<!-- Progress bar in title bar -->
|
||||
<div
|
||||
v-if="isShowprogress"
|
||||
class="progress-container"
|
||||
>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${progress}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="progress-text">{{ progress }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="title-right"
|
||||
>
|
||||
<div class="window-controls">
|
||||
<button
|
||||
class="control-button pin-button"
|
||||
:class="{ active: isAlwaysOnTop }"
|
||||
:title="$t('titleBar.alwaysOnTop')"
|
||||
@click="setAlwaysOnTop"
|
||||
>
|
||||
<PinIcon
|
||||
:color="isAlwaysOnTop ? '#CE6769' : '#6B7280'"
|
||||
:size="14"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="control-button minimize-button"
|
||||
:title="$t('titleBar.minimize')"
|
||||
@click="minimizeWindow"
|
||||
>
|
||||
<MinusIcon :size="14" />
|
||||
</button>
|
||||
<button
|
||||
class="control-button mini-button"
|
||||
:title="$t('titleBar.miniWindow')"
|
||||
@click="openMiniWindow"
|
||||
>
|
||||
<ShrinkIcon :size="14" />
|
||||
</button>
|
||||
<button
|
||||
class="control-button close-button"
|
||||
:title="$t('titleBar.close')"
|
||||
@click="closeWindow"
|
||||
>
|
||||
<XIcon :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MinusIcon, PinIcon, ShrinkIcon, XIcon } from 'lucide-vue-next'
|
||||
import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
import { osGlobal } from '@/utils/global'
|
||||
|
||||
const isShowprogress = ref(false)
|
||||
const progress = ref(0)
|
||||
const isAlwaysOnTop = ref(false)
|
||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||
|
||||
function setAlwaysOnTop () {
|
||||
isAlwaysOnTop.value = !isAlwaysOnTop.value
|
||||
window.electron.sendRPC(IRPCActionType.MAIN_WINDOW_ON_TOP)
|
||||
}
|
||||
|
||||
function minimizeWindow () {
|
||||
window.electron.sendRPC(IRPCActionType.MINIMIZE_WINDOW)
|
||||
}
|
||||
|
||||
function openMiniWindow () {
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
|
||||
}
|
||||
|
||||
function closeWindow () {
|
||||
window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
|
||||
}
|
||||
|
||||
const uploadProcessHandler = (data: { progress: number }) => {
|
||||
isShowprogress.value = data.progress !== 100 && data.progress !== 0
|
||||
progress.value = data.progress
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.electron.ipcRendererOn('updateProgress', uploadProcessHandler)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.electron.ipcRendererRemoveAllListeners('updateProgress')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32px;
|
||||
background: var(--color-background-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
z-index: 1000;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.title-bar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--color-border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-success);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
min-width: 35px;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: var(--color-surface-elevated);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.pin-button.active {
|
||||
color: var(--color-accent);
|
||||
background: var(--color-accent)20;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="title-bar" data-drag-region>
|
||||
<div class="title-bar-content">
|
||||
<div v-if="osGlobal !== 'darwin'" class="title-left">
|
||||
<div class="app-icon">
|
||||
<img :src="defaultLogo" alt="App Icon" width="20" height="20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-center">
|
||||
<!-- Progress bar in title bar -->
|
||||
<div v-if="isShowprogress" class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: `${progress}%` }" />
|
||||
</div>
|
||||
<span class="progress-text">{{ progress }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-right">
|
||||
<div class="window-controls">
|
||||
<button
|
||||
class="control-button pin-button"
|
||||
:class="{ active: isAlwaysOnTop }"
|
||||
:title="$t('titleBar.alwaysOnTop')"
|
||||
@click="setAlwaysOnTop"
|
||||
>
|
||||
<PinIcon :color="isAlwaysOnTop ? '#CE6769' : '#6B7280'" :size="14" />
|
||||
</button>
|
||||
<button class="control-button minimize-button" :title="$t('titleBar.minimize')" @click="minimizeWindow">
|
||||
<MinusIcon :size="14" />
|
||||
</button>
|
||||
<button class="control-button mini-button" :title="$t('titleBar.miniWindow')" @click="openMiniWindow">
|
||||
<ShrinkIcon :size="14" />
|
||||
</button>
|
||||
<button class="control-button close-button" :title="$t('titleBar.close')" @click="closeWindow">
|
||||
<XIcon :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MinusIcon, PinIcon, ShrinkIcon, XIcon } from 'lucide-vue-next'
|
||||
import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
import { osGlobal } from '@/utils/global'
|
||||
|
||||
const isShowprogress = ref(false)
|
||||
const progress = ref(0)
|
||||
const isAlwaysOnTop = ref(false)
|
||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||
|
||||
function setAlwaysOnTop() {
|
||||
isAlwaysOnTop.value = !isAlwaysOnTop.value
|
||||
window.electron.sendRPC(IRPCActionType.MAIN_WINDOW_ON_TOP)
|
||||
}
|
||||
|
||||
function minimizeWindow() {
|
||||
window.electron.sendRPC(IRPCActionType.MINIMIZE_WINDOW)
|
||||
}
|
||||
|
||||
function openMiniWindow() {
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
|
||||
}
|
||||
|
||||
const uploadProcessHandler = (data: { progress: number }) => {
|
||||
isShowprogress.value = data.progress !== 100 && data.progress !== 0
|
||||
progress.value = data.progress
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.electron.ipcRendererOn('updateProgress', uploadProcessHandler)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.electron.ipcRendererRemoveAllListeners('updateProgress')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32px;
|
||||
background: var(--color-background-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
z-index: 1000;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.title-bar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--color-border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-success);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
min-width: 35px;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: var(--color-surface-elevated);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.pin-button.active {
|
||||
color: var(--color-accent);
|
||||
background: var(--color-accent) 20;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,101 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- MessageToast component -->
|
||||
<MessageToast ref="messageRef" />
|
||||
|
||||
<!-- ConfirmMessageBox component -->
|
||||
<ConfirmMessageBox
|
||||
:is-open="confirmVisible"
|
||||
:title="confirmOptions.title"
|
||||
:message="confirmOptions.message"
|
||||
:type="confirmOptions.type"
|
||||
:confirm-button-text="confirmOptions.confirmButtonText"
|
||||
:cancel-button-text="confirmOptions.cancelButtonText"
|
||||
:show-close="confirmOptions.showClose"
|
||||
:center="confirmOptions.center"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import useConfirm, { type ConfirmOptions } from '@/hooks/useConfirm'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
|
||||
import ConfirmMessageBox from './ConfirmMessageBox.vue'
|
||||
import MessageToast from './MessageToast.vue'
|
||||
|
||||
const messageRef = ref<InstanceType<typeof MessageToast> | null>(null)
|
||||
const confirmVisible = ref(false)
|
||||
const confirmOptions = reactive<ConfirmOptions>({
|
||||
message: '',
|
||||
title: 'Confirm',
|
||||
type: 'info',
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
showClose: true,
|
||||
center: false
|
||||
})
|
||||
|
||||
let confirmResolve: ((value: boolean) => void) | null = null
|
||||
|
||||
const handleConfirm = () => {
|
||||
confirmVisible.value = false
|
||||
if (confirmResolve) {
|
||||
confirmResolve(true)
|
||||
confirmResolve = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
confirmVisible.value = false
|
||||
if (confirmResolve) {
|
||||
confirmResolve(false)
|
||||
confirmResolve = null
|
||||
}
|
||||
}
|
||||
|
||||
const showConfirm = (options: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
Object.assign(confirmOptions, {
|
||||
title: 'Confirm',
|
||||
type: 'info',
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
showClose: true,
|
||||
center: false,
|
||||
...options
|
||||
})
|
||||
confirmResolve = resolve
|
||||
confirmVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize message service
|
||||
const { setMessageService } = useMessage()
|
||||
if (messageRef.value) {
|
||||
setMessageService({
|
||||
success: messageRef.value.success,
|
||||
error: messageRef.value.error,
|
||||
warning: messageRef.value.warning,
|
||||
info: messageRef.value.info
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize confirm service
|
||||
const { setConfirmService } = useConfirm()
|
||||
setConfirmService({
|
||||
confirm: showConfirm
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UIServiceProvider'
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<!-- MessageToast component -->
|
||||
<MessageToast ref="messageRef" />
|
||||
|
||||
<!-- ConfirmMessageBox component -->
|
||||
<ConfirmMessageBox
|
||||
:is-open="confirmVisible"
|
||||
:title="confirmOptions.title"
|
||||
:message="confirmOptions.message"
|
||||
:type="confirmOptions.type"
|
||||
:confirm-button-text="confirmOptions.confirmButtonText"
|
||||
:cancel-button-text="confirmOptions.cancelButtonText"
|
||||
:show-close="confirmOptions.showClose"
|
||||
:center="confirmOptions.center"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import useConfirm, { type ConfirmOptions } from '@/hooks/useConfirm'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
|
||||
import ConfirmMessageBox from './ConfirmMessageBox.vue'
|
||||
import MessageToast from './MessageToast.vue'
|
||||
|
||||
const messageRef = ref<InstanceType<typeof MessageToast> | null>(null)
|
||||
const confirmVisible = ref(false)
|
||||
const confirmOptions = reactive<ConfirmOptions>({
|
||||
message: '',
|
||||
title: 'Confirm',
|
||||
type: 'info',
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
showClose: true,
|
||||
center: false
|
||||
})
|
||||
|
||||
let confirmResolve: ((value: boolean) => void) | null = null
|
||||
|
||||
const handleConfirm = () => {
|
||||
confirmVisible.value = false
|
||||
if (confirmResolve) {
|
||||
confirmResolve(true)
|
||||
confirmResolve = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
confirmVisible.value = false
|
||||
if (confirmResolve) {
|
||||
confirmResolve(false)
|
||||
confirmResolve = null
|
||||
}
|
||||
}
|
||||
|
||||
const showConfirm = (options: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise(resolve => {
|
||||
Object.assign(confirmOptions, {
|
||||
title: 'Confirm',
|
||||
type: 'info',
|
||||
confirmButtonText: 'Confirm',
|
||||
cancelButtonText: 'Cancel',
|
||||
showClose: true,
|
||||
center: false,
|
||||
...options
|
||||
})
|
||||
confirmResolve = resolve
|
||||
confirmVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize message service
|
||||
const { setMessageService } = useMessage()
|
||||
if (messageRef.value) {
|
||||
setMessageService({
|
||||
success: messageRef.value.success,
|
||||
error: messageRef.value.error,
|
||||
warning: messageRef.value.warning,
|
||||
info: messageRef.value.info
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize confirm service
|
||||
const { setConfirmService } = useConfirm()
|
||||
setConfirmService({
|
||||
confirm: showConfirm
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UIServiceProvider'
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user