🚧 development of image display interfaces

This commit is contained in:
2024-12-28 21:02:59 +08:00
parent 69ee63ca4e
commit 6854e41b82
26 changed files with 601 additions and 59 deletions

12
components.d.ts vendored
View File

@@ -45,6 +45,7 @@ declare module 'vue' {
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
@@ -64,7 +65,10 @@ declare module 'vue' {
Carousel: typeof import('./src/components/MyUI/Carousel/Carousel.vue')['default']
Cascader: typeof import('./src/components/MyUI/Cascader/Cascader.vue')['default']
Checkbox: typeof import('./src/components/MyUI/Checkbox/Checkbox.vue')['default']
CheckCard: typeof import('./src/components/MyUI/CheckCard/CheckCard.vue')['default']
CheckOutlined: typeof import('@ant-design/icons-vue')['CheckOutlined']
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
CloseOutlined: typeof import('@ant-design/icons-vue')['CloseOutlined']
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
Col: typeof import('./src/components/MyUI/Grid/Col.vue')['default']
Collapse: typeof import('./src/components/MyUI/Collapse/Collapse.vue')['default']
@@ -74,9 +78,11 @@ declare module 'vue' {
CompareImage: typeof import('./src/views/Upscale/CompareImage.vue')['default']
Countdown: typeof import('./src/components/MyUI/Countdown/Countdown.vue')['default']
DatePicker: typeof import('./src/components/MyUI/DatePicker/DatePicker.vue')['default']
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
Descriptions: typeof import('./src/components/MyUI/Descriptions/Descriptions.vue')['default']
Dialog: typeof import('./src/components/MyUI/Dialog/Dialog.vue')['default']
Divider: typeof import('./src/components/MyUI/Divider/Divider.vue')['default']
DownloadOutlined: typeof import('@ant-design/icons-vue')['DownloadOutlined']
Drawer: typeof import('./src/components/MyUI/Drawer/Drawer.vue')['default']
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default']
@@ -90,9 +96,11 @@ declare module 'vue' {
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
Image: typeof import('./src/components/MyUI/Image/Image.vue')['default']
ImageShare: typeof import('./src/views/ImageShare/ImageShare.vue')['default']
ImageWaterfall: typeof import('./src/components/MyUI/Waterfall/ImageWaterfall.vue')['default']
Input: typeof import('./src/components/MyUI/Input/Input.vue')['default']
InputSearch: typeof import('./src/components/MyUI/InputSearch/InputSearch.vue')['default']
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
LazyImage: typeof import('./src/components/MyUI/LazyImage/LazyImage.vue')['default']
List: typeof import('./src/components/MyUI/List/List.vue')['default']
LoadingBar: typeof import('./src/components/MyUI/LoadingBar/LoadingBar.vue')['default']
LoadingGraphic: typeof import('./src/components/LoadingGraphic/LoadingGraphic.vue')['default']
@@ -113,6 +121,7 @@ declare module 'vue' {
Phoalbum: typeof import('./src/views/Album/Phoalbum/Phoalbum.vue')['default']
PhoneUpload: typeof import('./src/views/PhoneUpload/PhoneUpload.vue')['default']
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
PlusSquareOutlined: typeof import('@ant-design/icons-vue')['PlusSquareOutlined']
Popconfirm: typeof import('./src/components/MyUI/Popconfirm/Popconfirm.vue')['default']
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
Progress: typeof import('./src/components/MyUI/Progress/Progress.vue')['default']
@@ -135,6 +144,7 @@ declare module 'vue' {
Segmented: typeof import('./src/components/MyUI/Segmented/Segmented.vue')['default']
Select: typeof import('./src/components/MyUI/Select/Select.vue')['default']
SendOutlined: typeof import('@ant-design/icons-vue')['SendOutlined']
ShareAltOutlined: typeof import('@ant-design/icons-vue')['ShareAltOutlined']
Skeleton: typeof import('./src/components/MyUI/Skeleton/Skeleton.vue')['default']
Slider: typeof import('./src/components/MyUI/Slider/Slider.vue')['default']
Space: typeof import('./src/components/MyUI/Space/Space.vue')['default']
@@ -162,6 +172,6 @@ declare module 'vue' {
Video: typeof import('./src/components/MyUI/Video/Video.vue')['default']
VueCompareImage: typeof import('./src/components/VueCompareImage/VueCompareImage.vue')['default']
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
Waterfall: typeof import('./src/components/MyUI/Waterfall/Waterfall.vue')['default']
Waterfall: typeof import('./src/components/MyUI/Waterfall/ImageWaterfall.vue')['default']
}
}

View File

@@ -5,6 +5,7 @@ import pluginVue from "eslint-plugin-vue";
import {fileURLToPath} from "node:url";
import {dirname, resolve} from "path";
import {readFileSync} from "node:fs";
import vueI18n from '@intlify/eslint-plugin-vue-i18n';
// 动态读取 .eslintrc-auto-import.json 文件内容
const autoImportConfig = JSON.parse(
readFileSync(
@@ -14,6 +15,7 @@ const autoImportConfig = JSON.parse(
);
export default [
...vueI18n.configs['flat/recommended'],
{files: ["**/*.{js,mjs,cjs,ts,vue}"]},
{languageOptions: {globals: {...globals.browser, ...globals.node, ...autoImportConfig.globals}}},
pluginJs.configs.recommended,
@@ -37,6 +39,26 @@ export default [
"ignoreRestSiblings": true
}
],
'@intlify/vue-i18n/no-dynamic-keys': 'error',
'@intlify/vue-i18n/no-unused-keys': [
'error',
{
extensions: ['.ts', '.vue']
}
],
"@intlify/vue-i18n/no-duplicate-keys-in-locale": [
"error",
{
"ignoreI18nBlock": false
}
],
"@intlify/vue-i18n/no-raw-text": 'off',
},
settings: {
'vue-i18n': {
localeDir: '/src/locales/language/*.{ts}',
messageSyntaxVersion: '^9.0.0'
}
}
}
];

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@alova/adapter-axios": "^2.0.12",
"@ant-design/icons-vue": "^7.0.1",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@tensorflow/tfjs": "^4.22.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
"@tensorflow/tfjs-backend-webgpu": "^4.22.0",
@@ -53,8 +54,9 @@
"vite-plugin-node-polyfills": "^0.22.0",
"vue": "^3.5.13",
"vue-dompurify-html": "^5.2.0",
"vue-i18n": "^10.0.5",
"vue-i18n": "^11.0.1",
"vue-router": "^4.5.0",
"vue-waterfall-plugin-next": "^2.6.4",
"ws": "^8.18.0"
},
"devDependencies": {
@@ -63,12 +65,12 @@
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.14.0",
"sass": "^1.83.0",
"typescript": "^5.6.3",
"typescript-eslint": "^8.18.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.2",
"unplugin-vue-components": "^0.28.0",
"vite": "^6.0.5",
"vite": "^6.0.6",
"vite-plugin-bundle-obfuscator": "1.4.0",
"vite-plugin-chunk-split": "^0.5.0",
"vue-tsc": "2.1.10"
"vue-tsc": "2.2.0"
}
}

View File

@@ -3,9 +3,9 @@
:locale="lang.lang === 'en' ? enUS : zhCN"
:theme="app.themeConfig"
>
<router-view v-slot="{ Component, route }">
<router-view v-slot="{ Component }">
<transition name="animation" mode="out-in">
<component :is="Component" :key="route.path"/>
<component :is="Component"/>
</transition>
</router-view>
</AConfigProvider>

BIN
src/assets/gif/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

View File

@@ -0,0 +1 @@
<svg t="1735317945913" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="42606" width="200" height="200"><path d="M518.9632 930.6112a424.704 424.704 0 1 1 424.96-424.704 424.96 424.96 0 0 1-424.96 424.704z m0-757.248a332.544 332.544 0 1 0 332.8 332.8 332.8 332.8 0 0 0-332.8-332.8z" fill="#FC5B67" p-id="42607"></path><path d="M683.2128 551.9872H354.7648a46.08 46.08 0 0 1 0-92.16h328.448a46.08 46.08 0 0 1 0 92.16z" fill="#FC5B67" p-id="42608"></path></svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -0,0 +1 @@
<svg t="1735317843676" class="icon" viewBox="0 0 1040 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="37944" width="200" height="200"><path d="M32 512a496 488 90 1 0 976 0 496 488 90 1 0-976 0Z" fill="#3770EB" p-id="37945"></path><path d="M485.344 755.968L251.504 522.16l98.576-98.592 140.288 140.272L736.864 317.392l93.536 93.52-345.072 345.056z" fill="#FFFFFF" p-id="37946"></path></svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@@ -0,0 +1 @@
<svg t="1735315086517" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14336" width="200" height="200"><path d="M736 336zM336 736h-0.01 0.02-0.01zM688 736h0.01-0.01zM736 688z" fill="#666666" p-id="14337"></path><path d="M512 64C264.58 64 64 264.58 64 512s200.58 448 448 448 448-200.57 448-448S759.42 64 512 64z m220.62 346.18L472.4 670.39a40 40 0 0 1-56.57 0L291.38 545.94a40 40 0 0 1 0-56.57 40 40 0 0 1 56.57 0l96.17 96.17 231.93-231.93a40 40 0 0 1 56.57 0 40 40 0 0 1 0 56.57z" fill="#999999" p-id="14338"></path><path d="M732.62 353.61a40 40 0 0 0-56.57 0L444.12 585.54 348 489.37a40 40 0 0 0-56.57 0 40 40 0 0 0 0 56.57l124.4 124.45a40 40 0 0 0 56.57 0l260.22-260.21a40 40 0 0 0 0-56.57z" fill="#FFFFFF" p-id="14339"></path></svg>

After

Width:  |  Height:  |  Size: 780 B

View File

@@ -0,0 +1 @@
<svg t="1735358313214" class="icon" viewBox="0 0 1166 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4000" width="200" height="200"><path d="M842.731517 407.355404a109.993623 109.993623 0 0 0 0-155.693942c-42.970688-42.897198-112.576274-42.844705-155.504967 0.094487-42.918195 42.949691-42.918195 112.555276 0 155.504967 42.918195 42.939192 112.523781 42.981187 155.504967 0.094488zM619.699683 768.076115l3.370044-4.304418-4.28342 4.304418-36.556056-46.918154-173.97197-223.934712a54.529625 54.529625 0 0 0-87.442673 0L72.818151 797.325159V146.549679a68.849688 68.849688 0 0 1 64.293304-73.12261h391.754514l31.49574-73.427069H145.636302C65.049202 0.503932 0 65.962578 0 146.549679v731.205101c0.167977 80.5871 65.364159 145.909265 145.940761 146.24522h294.758133l204.911285-201.394261-25.910496-54.529624z" fill="#A9A9A9" p-id="4001"></path><path d="M1020.650953 0H698.155572l-38.057353 73.427069h360.552734a78.602869 78.602869 0 0 1 72.818151 73.12261v648.045849l-175.189805-223.934712a57.920666 57.920666 0 0 0-95.67356-7.328009l-120.450208 121.048628 67.148918 75.474292-143.904037 264.144273h395.250541c80.461117-0.503932 145.468325-65.784102 145.636302-146.24522V146.549679C1166.287255 65.962578 1101.238053 0.503932 1020.650953 0z" fill="#A9A9A9" p-id="4002"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg t="1735134309122" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8818" width="200" height="200"><path d="M512 1024c282.760533 0 512-229.239467 512-512 0-282.760533-229.239467-512-512-512C229.239467 0 0 229.239467 0 512c0 282.760533 229.239467 512 512 512z m103.8336-680.618667c4.983467 5.597867 23.722667 25.2928 5.802667 37.5808-17.885867 12.288-33.450667-10.171733-39.150934-16.554666l-47.4112-51.541334v249.070934s0 30.651733-23.2448 31.095466c-23.278933 0.477867-23.278933-32.938667-23.278933-32.938666V312.900267l-47.377067 51.5072c-5.7344 6.382933-29.354667 22.493867-39.594666 10.308266-10.24-12.219733 0.443733-25.736533 5.461333-31.300266l81.5104-93.184a31.163733 31.163733 0 0 1 46.523733 0.034133l80.759467 93.149867z m104.721067 373.691734v-157.4912c0-17.408-1.365333-31.505067 15.940266-31.505067 17.271467 0 15.36 14.097067 15.36 31.505067v157.4912a31.402667 31.402667 0 0 1-31.300266 31.505066H304.5376a31.402667 31.402667 0 0 1-31.300267-31.505066v-157.4912c0-17.408 0.341333-31.505067 17.6128-31.505067 17.3056 0 17.169067 14.097067 17.169067 31.505067v157.4912H720.554667z" fill="#8FD2C7" p-id="8819"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,187 @@
<template>
<div class="check-card" :class="{ 'selected': isSelected }" @click="handleClick" :style="cardStyle">
<div class="hover-circle" @click.stop="toggleSelection"
v-if="showHoverCircle"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }">
<img :src="greyComplete" alt="Hover" class="hover-icon"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }"/>
</div>
<div class="card-content">
<slot></slot>
</div>
<div class="card-selected-icon" v-if="isSelected" :class="iconPositionClass"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }">
<img :src="icon" alt="Selected" class="selected-icon"
@click.stop="toggleSelection"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }"/>
<img :src="cancel" alt="Delete" class="delete-icon" @click.stop="toggleSelection"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }"/>
</div>
</div>
</template>
<script setup lang="ts">
import complete from '@/assets/svgs/complete.svg';
import cancel from '@/assets/svgs/cancle.svg';
import greyComplete from '@/assets/svgs/grey-complete.svg';
interface Props {
value: string | number;
modelValue?: (string | number)[];
icon?: string;
iconPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
margin?: string;
borderRadius?: string;
backgroundColor?: string;
showHoverCircle?: boolean; // 控制是否显示悬停圆环
iconSize?: number; // 控制图标大小
}
const props = withDefaults(defineProps<Props>(), {
icon: complete,
iconPosition: 'top-left',
margin: '16px',
borderRadius: '8px',
backgroundColor: '#e5eeff',
showHoverCircle: true, // 默认显示悬停圆环
iconSize: 24, // 默认图标大小
});
const emits = defineEmits(['update:modelValue']);
const isSelected = computed(() => {
return props.modelValue?.includes(props.value);
});
const iconPositionClass = computed(() => {
return `icon-${props.iconPosition}`;
});
const cardStyle = computed(() => {
return {
margin: props.margin,
borderRadius: props.borderRadius,
backgroundColor: props.backgroundColor,
};
});
function handleClick() {
if (!props.showHoverCircle) {
toggleSelection();
}
}
function toggleSelection() {
if (isSelected.value) {
emits('update:modelValue', props.modelValue?.filter((val) => val !== props.value));
} else {
emits('update:modelValue', [...(props.modelValue || []), props.value]);
}
}
</script>
<style scoped lang="scss">
.check-card {
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.3s, background-color 0.3s;
overflow: visible; /* Ensure the icon is not cut off */
}
.check-card.selected {
border: 1px solid rgba(125, 167, 255, 0.68);
box-shadow: 0 0 2px rgba(77, 167, 255, 0.89);
padding: 10px;
}
.card-content {
flex: 1;
}
.card-selected-icon {
position: absolute;
background-color: white;
border-radius: 50%; /* Optional: Make it circular */
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc; /* Optional: Add a border */
}
.icon-top-left {
top: 3px;
left: 3px;
}
.icon-top-right {
top: 3px;
right: 3px;
}
.icon-bottom-left {
bottom: 3px;
left: 3px;
}
.icon-bottom-right {
bottom: 3px;
right: 3px;
}
.card-selected-icon img {
z-index: 3; /* Ensure the icon is on top */
}
.selected-icon {
display: block;
}
.delete-icon {
display: none;
cursor: pointer;
}
.card-selected-icon:hover .selected-icon {
display: none;
}
.card-selected-icon:hover .delete-icon {
display: block;
}
.hover-circle {
position: absolute;
top: 3px; /* 与 .card-selected-icon 的 top 值相同 */
left: 3px; /* 与 .card-selected-icon 的 left 值相同 */
border: 2px solid white; /* 与 .card-selected-icon 的 border 值相同 */
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-color: transparent;
z-index: 1; /* Ensure the hover circle is on top */
opacity: 0;
transform: scale(0); /* 初始状态为0 */
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; /* 添加动画效果 */
}
.hover-circle .hover-icon {
display: none;
}
.check-card:hover .hover-circle {
opacity: 1; /* 鼠标悬停时变为可见 */
transform: scale(1); /* 鼠标悬停时放大到1倍 */
}
.check-card:hover .hover-circle .hover-icon {
display: block;
}
.check-card.selected:hover .hover-circle {
display: none;
}
</style>

View File

@@ -156,7 +156,7 @@ function onChecked() {
&:not(.checkbox-disabled):hover {
.checkbox-box {
border-color: @themeColor;
border-color: #40a9ff;
}
}
@@ -195,8 +195,8 @@ function onChecked() {
}
.checkbox-checked {
background-color: @themeColor;
border-color: @themeColor;
background-color: #40a9ff;
border-color: #40a9ff;
&::after {
opacity: 1;
@@ -211,7 +211,7 @@ function onChecked() {
left: 50%;
width: 8px;
height: 8px;
background-color: @themeColor;
background-color: #40a9ff;
border: 0;
transform: translate(-50%, -50%) scale(1);
opacity: 1;

View File

@@ -70,7 +70,7 @@ const showAfter = computed(() => {
return slotsExist.addonAfter || props.addonAfter;
});
const lazyInput = computed(() => {
return 'lazy' in props.valueModifiers;
return 'lazy.ts' in props.valueModifiers;
});
function onInput(e: InputEvent) {

View File

@@ -70,7 +70,7 @@ const showBefore = computed(() => {
return slotsExist.addonBefore || props.addonBefore;
});
const lazyInput = computed(() => {
return 'lazy' in props.valueModifiers;
return 'lazy.ts' in props.valueModifiers;
});
function onInput(e: InputEvent) {

View File

@@ -67,7 +67,7 @@ const showCountNum = computed(() => {
return props.value.length;
});
const lazyTextarea = computed(() => {
return 'lazy' in props.valueModifiers;
return 'lazy.ts' in props.valueModifiers;
});
watch(
() => props.value,

View File

@@ -9,5 +9,5 @@ import lazyLoad from "@/directives/v-lazy-load.ts";
*/
export const registerDirectives = (app: any) => {
app.directive('click-outside', clickOutside);
app.directive('lazy-load', lazyLoad);
app.directive('lazy.ts-load', lazyLoad);
};

View File

@@ -11,13 +11,13 @@
@select="handleClick"
>
<AMenuItemGroup :title="t('album.photo')" type="group" key="photo">
<AMenuItem :title="t('album.allAlbums')" key="photo/all">
<AMenuItem :title="t('album.allAlbums')" key="photo/all" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="allPhoto"/>
</template>
<span class="ant-menu-item-title">{{ t('album.allAlbums') }}</span>
</AMenuItem>
<AMenuItem :title="t('album.recentUploads')" key="photo/recent">
<AMenuItem :title="t('album.recentUploads')" key="photo/recent" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="recentUpload"/>
</template>
@@ -25,25 +25,25 @@
</AMenuItem>
</AMenuItemGroup>
<AMenuItemGroup :title="t('album.albums')" key="album">
<AMenuItem :title="t('album.albums')" key="album/albums">
<AMenuItem :title="t('album.albums')" key="album/albums" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="album"/>
</template>
<span class="ant-menu-item-title">{{ t('album.albums') }}</span>
</AMenuItem>
<AMenuItem :title="t('album.peopleAlbums')" key="album/people">
<AMenuItem :title="t('album.peopleAlbums')" key="album/people" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="peopleAlbum"/>
</template>
<span class="ant-menu-item-title">{{ t('album.peopleAlbums') }}</span>
</AMenuItem>
<AMenuItem :title="t('album.locationAlbums')" key="album/location">
<AMenuItem :title="t('album.locationAlbums')" key="album/location" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="loactionAlbum"/>
</template>
<span class="ant-menu-item-title">{{ t('album.locationAlbums') }}</span>
</AMenuItem>
<AMenuItem :title="t('album.thingsAlbums')" key="album/thing">
<AMenuItem :title="t('album.thingsAlbums')" key="album/thing" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="thingAlbum"/>
</template>
@@ -51,19 +51,19 @@
</AMenuItem>
</AMenuItemGroup>
<ADivider/>
<AMenuItem :title="t('album.recyclingBin')" key="photo/share">
<AMenuItem :title="t('album.recyclingBin')" key="photo/share" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="share"/>
</template>
<span class="ant-menu-item-title">{{ t('album.share') }}</span>
</AMenuItem>
<AMenuItem :title="t('album.recyclingBin')" key="photo/recycling">
<AMenuItem :title="t('album.recyclingBin')" key="photo/recycling" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="recyclingbin"/>
</template>
<span class="ant-menu-item-title">{{ t('album.recyclingBin') }}</span>
</AMenuItem>
<AMenuItem :title="t('album.recyclingBin')" key="photo/upscale">
<AMenuItem :title="t('album.recyclingBin')" key="photo/upscale" :style="menuCSSStyle">
<template #icon>
<AAvatar shape="square" size="small" :src="ai"/>
</template>
@@ -101,8 +101,6 @@ const {t} = useI18n();
const router = useRouter();
const route = useRoute();
/**
* handle click event of menu item
* @param key
@@ -111,6 +109,32 @@ function handleClick({key}) {
router.push(`/main/${key}`);
}
const menuCSSStyle: any = reactive({
display: 'flex',
alignItems: 'center',
});
watch(
() => route.path,
() => {
scrollToSelectedMenuItem();
}
);
// scroll to selected menu item
function scrollToSelectedMenuItem() {
nextTick(() => {
const selected = document.querySelector(`.ant-menu-item-selected`);
if (selected) {
selected.scrollIntoView({behavior: 'smooth', block: 'center'});
}
});
}
onMounted(() => {
scrollToSelectedMenuItem();
});
</script>
<style scoped lang="scss" src="./index.scss">

View File

@@ -2,7 +2,7 @@
width: 220px;
height: calc(100vh - 271px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
overflow-y: scroll;
overflow-y: auto;
overflow-x: hidden;
background-color: #fff;

View File

@@ -28,17 +28,13 @@
</template>
<script setup lang="ts">
import useStore from "@/store";
// import {h, onMounted, onUnmounted} from "vue";
import Header from "@/layout/default/Header/Header.vue";
// import {notification} from "ant-design-vue";
// import {SmileOutlined} from "@ant-design/icons-vue";
import Sidebar from "@/layout/default/Sidebar/Sidebar.vue";
import {useI18n} from "vue-i18n";
import translation from '@/assets/svgs/translation.svg';
import darkMode from '@/assets/svgs/dark-mode.svg';
import other from '@/assets/svgs/other.svg';
// const websocket = useStore().websocket;
// const userInfo = useStore().user;
const lang = useStore().lang;
const {locale} = useI18n();
@@ -50,24 +46,6 @@ async function changeLanguage() {
locale.value = lang.lang;
}
// const wsOptions = {
// url: import.meta.env.VITE_MESSAGE_SOCKET_URL + "?user_id=" + userInfo.user.uid + "&token=" + userInfo.user.access_token,
// };
//
// onMounted(() => {
// websocket.initialize(wsOptions);
// websocket.on("message", async (data: any) => {
// notification.open({
// message: '消息来了',
// description:
// data,
// icon: () => h(SmileOutlined, {style: 'color: #108ee9'}),
// });
// });
// });
// onUnmounted(() => {
// websocket.close(false);
// });
</script>
<style scoped lang="less" src="./index.scss">

View File

@@ -1,9 +1,276 @@
<template>
<div class="all-photo">
<div class="photo-header">
<AButton type="primary" shape="round" size="middle">
<template #icon>
<PlusOutlined/>
</template>
上传照片
</AButton>
<AButton type="default" shape="round" size="middle">
<template #icon>
<PlusSquareOutlined/>
</template>
创建相册
</AButton>
</div>
<transition name="slide-fade">
<div v-show="selected.length !== 0" class="photo-toolbar-header">
<div class="photo-toolbar-left">
<AButton type="text" shape="circle" size="large" class="photo-toolbar-btn">
<template #icon>
<CloseOutlined class="photo-toolbar-icon"/>
</template>
</AButton>
<span style="font-size: 16px;font-weight: bold">
已选择 {{ selected.length }} 张照片
</span>
<AButton type="text" shape="default" class="photo-toolbar-btn" size="middle">
全选
</AButton>
</div>
<div class="photo-toolbar-right">
<AButton type="text" shape="default" size="middle" class="photo-toolbar-btn">
<template #icon>
<PlusSquareOutlined class="photo-toolbar-icon"/>
</template>
添加到
</AButton>
<AButton type="text" shape="default" size="middle" class="photo-toolbar-btn">
<template #icon>
<DownloadOutlined class="photo-toolbar-icon"/>
</template>
下载原图
</AButton>
<AButton type="text" shape="default" size="middle" class="photo-toolbar-btn">
<template #icon>
<ShareAltOutlined class="photo-toolbar-icon"/>
</template>
分享
</AButton>
<AButton type="text" shape="default" size="middle" class="photo-toolbar-btn">
<template #icon>
<DeleteOutlined class="photo-toolbar-icon"/>
</template>
删除
</AButton>
</div>
</div>
</transition>
<div class="photo-list">
<ATabs size="small" :tabBarGutter="50" type="line" tabPosition="top" :tabBarStyle="{position:'unset'}"
style="width: 99%;">
<template #rightExtra>
<ASwitch size="small" v-model:checked="switchValue"/>
</template>
<ATabPane key="1" tab="全部">
<div style="width:100%;height:100%;" v-if="images.length !== 0">
<span style="margin-left: 10px;font-size: 13px">2024年12月27日 星期日</span>
<AImagePreviewGroup>
<Waterfall :list="images"
:backgroundColor="`transparent`"
:width="400"
:gutter="15"
align="left"
:lazyload="true"
:animationDelay="300"
:animationDuration="1000"
:animationCancel="false"
:hasAroundGutter="true"
rowKey="id"
:imgSelector="'src'"
:loadProps="loadProps"
:breakpoints="breakpoints">
<template #default="{ item, url, index }">
<CheckCard :key="index"
margin="0"
border-radius="0"
v-model="selected"
:showHoverCircle="true"
:iconSize="20"
:value="url">
<AImage :src="url"
:alt="item.title"
:key="index"
:previewMask="false"
loading="lazy"/>
</CheckCard>
</template>
</Waterfall>
</AImagePreviewGroup>
</div>
</ATabPane>
<ATabPane key="2" tab="视频">
<div style="width:100%;height:100%;">
</div>
</ATabPane>
<ATabPane key="3" tab="动图">
</ATabPane>
<ATabPane key="4" tab="截图">
</ATabPane>
</ATabs>
</div>
</div>
</template>
<script setup lang="ts">
import {Waterfall} from 'vue-waterfall-plugin-next';
import 'vue-waterfall-plugin-next/dist/style.css';
import loading from '@/assets/gif/loading.gif';
import error from '@/assets/svgs/no-image.svg';
const selected = ref<(string | number)[]>([]);
const switchValue = ref<boolean>(false);
const breakpoints = reactive({
breakpoints: {
1200: {
// 当屏幕宽度小于等于1200
rowPerView: 4,
},
800: {
// 当屏幕宽度小于等于800
rowPerView: 3,
},
500: {
// 当屏幕宽度小于等于500
rowPerView: 2,
},
},
});
const loadProps = reactive({
loading,
error,
ratioCalculator: (_width: number, _height: number) => {
// 我设置了最小宽高比
const minRatio = 3 / 4;
const maxRatio = 4 / 3;
return Math.random() > 0.5 ? minRatio : maxRatio;
},
});
const images = ref<any[]>([]);
function loadImages() {
for (let i = 1; i < 10; i++) {
images.value.push({
title: `image-${i}`,
link: '',
src: `https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.5/${i}.jpg`,
tag: '全部',
date: '2022-01-01',
});
}
}
onBeforeMount(() => { // 组件已完成响应式状态设置但未创建DOM节点
loadImages();
});
</script>
<style scoped lang="scss" src="./index.scss">
<style scoped lang="scss">
.all-photo {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.photo-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
}
.photo-toolbar-header {
position: fixed;
width: calc(100% - 220px);
height: 70px;
top: 70px;
z-index: 3;
display: flex;
box-sizing: border-box;
justify-content: space-between;
align-items: center;
background-image: linear-gradient(45deg, #5789ff, #5c7bff 100%);
color: #fff;
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .06);
padding: 0 20px;
.photo-toolbar-left {
width: 50%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 20px;
}
.photo-toolbar-right {
height: 100%;
width: 50%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 30px;
}
.photo-toolbar-icon {
font-size: 20px;
font-weight: bold;
color: #fff;
}
.photo-toolbar-btn {
font-size: 16px;
font-weight: bold;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
.photo-list {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
height: calc(100% - 65px);
}
}
.slide-fade-enter-active, .slide-fade-leave-active {
transition: all 0.5s ease;
}
.slide-fade-enter-from, .slide-fade-leave-to { /* .slide-fade-leave-active 在离开之前 */
transform: translateY(-20px);
opacity: 0;
}
.slide-fade-enter-from {
transform: translateY(-30px);
opacity: 0;
}
.slide-fade-enter-to {
transform: translateY(0);
opacity: 1;
}
</style>

View File

@@ -1,7 +1,35 @@
<template>
<div class="recent-upload">
<div class="photo-header">
<AButton type="primary" shape="round" size="middle">
<template #icon>
<PlusOutlined/>
</template>
上传照片
</AButton>
<AButton type="default" shape="round" size="middle">
<template #icon>
<PlusSquareOutlined/>
</template>
创建相册
</AButton>
</div>
<div class="photo-list">
<CheckCard style="margin-top: 20px;" v-for="item in items" :key="item.id" :value="item.id" v-model="selectedItems">
{{ item.name }}
</CheckCard>
<div>Selected Items: {{ selectedItems }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const items = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
]);
const selectedItems = ref<(string | number)[]>([]);
</script>
<style scoped lang="scss" src="./index.scss">

View File

@@ -0,0 +1,19 @@
.recent-upload {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
.photo-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
}
}

View File

@@ -5,14 +5,14 @@
<span class="upscale-params-title">类型:</span>
<ASelect style="width: 100%" size="large"
v-model:value="upscale.model_type"
:options="upscale.TypeData.map(item => ({label: item, value: item}))">
:options="upscale.TypeData.map(item => ({label: item, value: item,key: item}))">
</ASelect>
</div>
<div class="upscale-params-item-content">
<span class="upscale-params-title">模型:</span>
<ASelect style="width: 100%" size="large"
v-model:value="upscale.model"
:options="upscale.modes.map((item: any) => ({label: item, value: item}))">
:options="upscale.modes.map((item: any) => ({label: item, value: item,key: item}))">
</ASelect>
</div>
</div>
@@ -21,7 +21,7 @@
<span class="upscale-params-title">比列:</span>
<ASelect style="width: 100%" size="large"
v-model:value="upscale.factor"
:options="upscale.scales.map((item: any) => ({label: item, value: item}))">
:options="upscale.scales.map((item: any) => ({label: item, value: item,key: item}))">
</ASelect>
</div>
@@ -29,7 +29,7 @@
<span class="upscale-params-title">分块大小:</span>
<ASelect style="width: 100%" size="large"
v-model:value="upscale.tile_size"
:options="upscale.tileSize.map((item: any) => ({label: item, value: item}))">
:options="upscale.tileSize.map((item: any) => ({label: item, value: item,key: item}))">
</ASelect>
</div>
@@ -39,14 +39,14 @@
<span class="upscale-params-title">重复:</span>
<ASelect style="width: 100%" size="large"
v-model:value="upscale.min_lap"
:options="upscale.overlapList.map((item: any) => ({label: item, value: item}))">
:options="upscale.overlapList.map((item: any) => ({label: item, value: item,key: item}))">
</ASelect>
</div>
<div class="upscale-params-item-content">
<span class="upscale-params-title">运行环境</span>
<ASelect style="width: 100%" size="large"
v-model:value="upscale.backend"
:options="upscale.backendList.map((item: any) => ({label: item, value: item}))">
:options="upscale.backendList.map((item: any) => ({label: item, value: item,key: item}))">
</ASelect>
</div>
</div>

View File

@@ -174,7 +174,7 @@ export default defineConfig(({mode}: { mode: string }): object => {
write: true, // 启用将构建后的文件写入磁盘
emptyOutDir: true, // 构建时清空该目录
brotliSize: true, // 启用 brotli 压缩大小报告
chunkSizeWarningLimit: 10000, // chunk 大小警告的限制
chunkSizeWarningLimit: 15000, // chunk 大小警告的限制
watch: null, // 设置为 {} 则会启用 rollup 的监听器
rollupOptions: { // 自定义底层的 Rollup 打包配置
output: {