This commit is contained in:
itcsx committed 2026-09-22 23:09:11 +08:00
1 parent 4adae1bd7d
commit adcf94df4a
17 files changed
+274 -97

No files matched your search

@@ -1,19 +1,21 @@
using CloudBuilder.Core.DatabaseAccessor.Entity;
using Microsoft.AspNetCore.Authorization;
using CloudBuilder.Core.DatabaseAccessor;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.Extensions;
using CloudBuilder.Core.Policy;
using CloudBuilder.Core.Service;
using CloudBuilder.Master.Data;
using CloudBuilder.Master.Entity;
using CloudBuilder.Master.Policy;
using CloudBuilder.Master.SearchCriteria;
using CloudBuilder.Master.Data;
using CloudBuilder.Core.Service;
using System.Diagnostics;
using CloudBuilder.Security.Service;
using Microsoft.Extensions.DependencyInjection;
using CloudBuilder.Security.Entity;
using CloudBuilder.Security.Utility;
using CloudBuilder.Security.Policy;
using CloudBuilder.Core.Policy;
using CloudBuilder.Security.Service;
using CloudBuilder.Security.Utility;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;
namespace CloudBuilder.Master.Service
{
public class PublishPhotoService : IPublishPhotoService
@@ -73,13 +75,14 @@ namespace CloudBuilder.Master.Service
data.PublishPhoto = publish;
IRepository<PublishPhotoDetailEntity> repositoryPublishPhotoDetail = repository.GetRepository<IRepository<PublishPhotoDetailEntity>>();
data.Details = repositoryPublishPhotoDetail.DetachedEntities.Where(x => x.PublishName == publish.PublishName).OrderBy(x => x.OrderBy).ToArray();
data.Details = repositoryPublishPhotoDetail.DetachedEntities.Where(x => x.PublishName == publish.PublishName).OrderBy(x => x.OrderBy).Select(x => new PublishPhotoDetailDisplayData(x)).ToArray();
bool isAttached = Debugger.IsAttached;
if (data.Details != null || data.Details.Length > 0)
foreach (var item in data.Details)
{
item.FormattedPic = PackImageScareFileName(isAttached, item.FileName);
item.FileName = PackImageFileName(isAttached, item.FileName);
}
@@ -87,10 +90,20 @@ namespace CloudBuilder.Master.Service
return data;
}
private string PackImageFileName(bool isAttached,string fileName)
private string PackImageScareFileName(bool isAttached, string fileName)
{
return (isAttached ? service.Configuration[AppsettingsPolicy.ApplicationUrl] : "")
+ fileName?.Replace("\\", "/");
var phy = service.Configuration[ServiceSettingsPolicy.PhysicalFileDirectory] ?? "";
var backup = (ServiceSettingsPolicy.GetDmsFileBackupDirectory(service) ?? "").Replace(phy ?? "", "");
var scale = (ServiceSettingsPolicy.GetDmsFileScaleDirectory(service) ?? "").Replace(phy ?? "", "");
return (isAttached ? service.Configuration[AppsettingsPolicy.ApplicationUrl] : "")
+ fileName?.Replace("\\", "/").Replace(backup.Replace("\\", "/"), scale.Replace("\\", "/"));
}
private string PackImageFileName(bool isAttached, string fileName)
{
return (isAttached ? service.Configuration[AppsettingsPolicy.ApplicationUrl] : "")
+ fileName?.Replace("\\", "/");
}
[PermissionOperate(name: MasterPermissionPolicy.PublishPhoto, operate: Operate.Find)]
@@ -336,9 +349,9 @@ namespace CloudBuilder.Master.Service
}
[AllowAnonymous]
public PublishPhotoDetailEntity[] FindPublishPhotoDetailByPublishName(string publishName)
public PublishPhotoDetailDisplayData[] FindPublishPhotoDetailByPublishName(string publishName)
{
List<PublishPhotoDetailEntity> publishPhotoDetails = new List<PublishPhotoDetailEntity>();
List<PublishPhotoDetailDisplayData> publishPhotoDetails = new List<PublishPhotoDetailDisplayData>();
if (string.IsNullOrEmpty(publishName)) return publishPhotoDetails.ToArray();
IRepository<PublishPhotoEntity> repositoryPublishPhoto = repository.GetRepository<IRepository<PublishPhotoEntity>>();
@@ -348,7 +361,7 @@ namespace CloudBuilder.Master.Service
return publishPhotoDetails.ToArray();
IRepository<PublishPhotoDetailEntity> repositoryPublishPhotoDetail = repository.GetRepository<IRepository<PublishPhotoDetailEntity>>();
PublishPhotoDetailEntity[] dtls = repositoryPublishPhotoDetail.DetachedEntities.Where(x => x.PublishName == publishName).OrderBy(x => x.OrderBy).ToArray();
PublishPhotoDetailDisplayData[] dtls = repositoryPublishPhotoDetail.DetachedEntities.Where(x => x.PublishName == publishName).OrderBy(x => x.OrderBy).Select(x => new PublishPhotoDetailDisplayData(x)).ToArray();
if (dtls == null || dtls.Length == 0) return publishPhotoDetails.ToArray();
@@ -356,7 +369,7 @@ namespace CloudBuilder.Master.Service
foreach (var item in dtls)
{
if (item.FileName.ToLower().StartsWith("http")) continue;
item.FormattedPic = PackImageScareFileName(isAttached, item.FileName);
item.FileName = PackImageFileName(isAttached, item.FileName);
}
@@ -364,16 +377,16 @@ namespace CloudBuilder.Master.Service
}
[AllowAnonymous]
public PublishPhotoDetailEntity[] FindPublishPhotoDetail()
public PublishPhotoDetailDisplayData[] FindPublishPhotoDetail()
{
PublishPhotoEntity publishPhoto = repository.DetachedEntities.Where(x => x.OwnerName == service.UserId && x.DefaultIndc == YesNoPolicy.YES).FirstOrDefault();
List<PublishPhotoDetailEntity> publishPhotoDetails = new List<PublishPhotoDetailEntity>();
List<PublishPhotoDetailDisplayData> publishPhotoDetails = new List<PublishPhotoDetailDisplayData>();
if (publishPhoto == null)
return publishPhotoDetails.ToArray();
IRepository<PublishPhotoDetailEntity> repositoryPublishPhotoDetail = repository.GetRepository<IRepository<PublishPhotoDetailEntity>>();
PublishPhotoDetailEntity[] dtls = repositoryPublishPhotoDetail.DetachedEntities.Where(x => x.PublishName == publishPhoto.PublishName).OrderBy(x => x.OrderBy).ToArray();
PublishPhotoDetailDisplayData[] dtls = repositoryPublishPhotoDetail.DetachedEntities.Where(x => x.PublishName == publishPhoto.PublishName).OrderBy(x => x.OrderBy).Select(x => new PublishPhotoDetailDisplayData(x)).ToArray();
if (dtls == null || dtls.Length == 0) return dtls;
@@ -382,6 +395,7 @@ namespace CloudBuilder.Master.Service
{
if (item.FileName.ToLower().StartsWith("http")) continue;
item.FormattedPic = PackImageScareFileName(isAttached, item.FileName);
item.FileName = (isAttached ? service.Configuration[AppsettingsPolicy.ApplicationUrl] : "") + item.FileName;
}
@@ -425,7 +439,7 @@ namespace CloudBuilder.Master.Service
var fileExtension = Path.GetExtension(dmsFile.OrderKey);
var filePathRel = Path.Combine(webRootPath.Substring(1), backup, dmsFile.CreatedBy, fileExtension.Replace(".", ""), "");
var filePath = '/'+filePathRel.Replace(Path.DirectorySeparatorChar, '/');
var filePath = '/' + filePathRel.Replace(Path.DirectorySeparatorChar, '/');
string path = Path.Combine(filePath, dmsFile.OrderKey);
@@ -13,7 +13,7 @@ namespace CloudBuilder.Master.Data
{
public PublishPhotoEntity? PublishPhoto { get; set; }
public PublishPhotoDetailEntity[]? Details { get; set; }
public PublishPhotoDetailDisplayData[]? Details { get; set; }
public PublishPhotoFamilyGroupDisplayData[]? FamilyGroups { get; set; }
}
@@ -0,0 +1,27 @@
using CloudBuilder.Core.Authorization;
using CloudBuilder.Master.Entity;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Master.Data
{
[NotMapped]
[TypeScript]
public class PublishPhotoDetailDisplayData : PublishPhotoDetailEntity
{
public PublishPhotoDetailDisplayData()
{
}
public PublishPhotoDetailDisplayData(PublishPhotoDetailEntity entity)
{
CopyFrom(entity);
}
public string? FormattedPic { get; set; }
}
}
@@ -15,7 +15,7 @@ namespace CloudBuilder.Master.Service
void InsertPublishPhotoDetail(PublishData publishData);
void InsertPublishPhotoDetailByFamilyGroup(PublishData publishData);
PublishPhotoDetailEntity[] FindPublishPhotoDetail();
PublishPhotoDetailEntity[] FindPublishPhotoDetailByPublishName(string publishName);
PublishPhotoDetailDisplayData[] FindPublishPhotoDetail();
PublishPhotoDetailDisplayData[] FindPublishPhotoDetailByPublishName(string publishName);
}
}
@@ -47,13 +47,13 @@ return Task.Run(() => serviceRequest.PostAsJsonAsync<PublishPhotoData,PublishP
{
Task.Run(() => serviceRequest.PostAsync("/PublishPhoto/Delete?key=" + key.ToString())).GetAwaiter().GetResult(); }
public PublishPhotoDetailEntity[] FindPublishPhotoDetailByPublishName(string publishName)
public PublishPhotoDetailDisplayData[] FindPublishPhotoDetailByPublishName(string publishName)
{
return Task.Run(() => serviceRequest.PostAsJsonAsync<PublishPhotoDetailEntity[]>("/PublishPhoto/FindPublishPhotoDetailByPublishName?publishName=" + publishName.ToString())).GetAwaiter().GetResult(); }
return Task.Run(() => serviceRequest.PostAsJsonAsync<PublishPhotoDetailDisplayData[]>("/PublishPhoto/FindPublishPhotoDetailByPublishName?publishName=" + publishName.ToString())).GetAwaiter().GetResult(); }
public PublishPhotoDetailEntity[] FindPublishPhotoDetail()
public PublishPhotoDetailDisplayData[] FindPublishPhotoDetail()
{
return Task.Run(() => serviceRequest.PostAsJsonAsync<PublishPhotoDetailEntity[]> ("/PublishPhoto/FindPublishPhotoDetail")).GetAwaiter().GetResult(); }
return Task.Run(() => serviceRequest.PostAsJsonAsync<PublishPhotoDetailDisplayData[]> ("/PublishPhoto/FindPublishPhotoDetail")).GetAwaiter().GetResult(); }
public void InsertPublishPhotoDetail(PublishData publishData)
{
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<NameOfLastUsedPublishProfile>D:\Docker\CloudBuilder\CloudBuilder.Web.Entry\Properties\PublishProfiles\FolderProfile4.pubxml</NameOfLastUsedPublishProfile>
<NameOfLastUsedPublishProfile>D:\Docker\CloudBuilder\CloudBuilder.Web.Entry\Properties\PublishProfiles\FolderProfile3.pubxml</NameOfLastUsedPublishProfile>
<ActiveDebugProfile>http</ActiveDebugProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
<History>True|2026-09-22T03:19:19.6087109Z||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
<History>True|2026-09-22T15:03:13.6509491Z||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
@@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>C:\inetpub\wwwroot\windows</_PublishTargetUrl>
<History>True|2026-09-22T07:18:53.4935889Z||;True|2026-09-22T11:24:43.8593406+08:00||;True|2026-09-19T09:55:09.0653660+08:00||;True|2026-09-19T09:53:13.8933944+08:00||;</History>
<History>True|2026-09-22T15:02:08.0902556Z||;True|2026-09-22T16:47:49.2530282+08:00||;True|2026-09-22T15:18:53.4935889+08:00||;True|2026-09-22T11:24:43.8593406+08:00||;True|2026-09-19T09:55:09.0653660+08:00||;True|2026-09-19T09:53:13.8933944+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
+20 -3
View File
@@ -1,10 +1,27 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/youjia.svg" />
<!--
这里的信息决定「浏览器标签页」和「微信收藏 / 转发卡片」长什么样:
卡片标题取 document.title(本文的 <title>,之后由 src/App.vue 按路由 meta 覆盖),
卡片副标题取 description,图标取 icon(微信不认 svg,所以用 png 的 logo)。
微信会缓存卡片信息:换过文案/图标后把下面的 ?v= 递增一位,促使它重新抓取。
-->
<link rel="icon" type="image/png" href="/images/logo.png?v=2" />
<link rel="shortcut icon" href="/images/logo.png?v=2" />
<link rel="apple-touch-icon" href="/images/logo.png?v=2" />
<!-- 手机把本站「添加到主屏幕」时的名称 -->
<meta name="apple-mobile-web-app-title" content="有家相册" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<title>有家相册</title>
<meta name="description" content="欢迎回家,记录温暖瞬间" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="有家相册" />
<meta property="og:title" content="有家相册" />
<meta property="og:description" content="欢迎回家,记录温暖瞬间" />
<!-- 转发到聊天的大图卡片用它:512×512(微信要求不小于 300×300),由品牌 svg 矢量渲染而来 -->
<meta property="og:image" content="/images/logo-512.png?v=2" />
</head>
<body>
<div id="app"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+100 -15
View File
@@ -1,9 +1,10 @@
<template>
<div class="psp">
<!-- 图片舞台 -->
<div class="psp-stage" ref="stageEl" :class="{ 'is-paused': !isPlaying && !!current }"
<div class="psp-stage" ref="stageEl"
:class="{ 'is-paused': !isPlaying && !!current, 'is-immersive': !controlsVisible }"
:style="{ '--psp-kb': interval + 0.8 + 's', '--psp-out-span': outPlan.span + 's' }"
:title="isPlaying ? '单击暂停' : '单击看大图'" @click="onStageClick">
:title="stageTitle" @click="onStageClick">
<!-- 组合类:多图同台 + 相框 / 气泡 / 花瓣 / 彩纸 / 聚光 / 灯柱 / 舞台 / 荧光棒(演唱会等 5 套) -->
<!-- out-in:上一屏彻底走完才让下一屏进场,两屏绝不重叠(逐张离场也等最后一张飞完) -->
<transition v-if="isShowcase" :name="'psp-fx-' + fxName" :duration="fxDuration" mode="out-in">
@@ -71,12 +72,25 @@
<PictureOutlined />
<p>暂无图片</p>
</div>
<!-- 控制区收起后画面上留一个恢复入口:整页用法没有弹窗右上角按钮,
否则收起后就再也点不回来了 -->
<button v-if="!controlsVisible" class="psp-restore" type="button" title="显示控制区"
@click.stop="toggleControls">
<EyeOutlined />
</button>
</div>
<!-- 控制条 -->
<footer class="psp-bar">
<!-- 控制条(收起后舞台吃掉这部分空间,图片最大化) -->
<footer v-if="controlsVisible" class="psp-bar">
<!-- 功能图标行 -->
<div class="psp-actions">
<!-- 隐藏/显示控制区:整页用法时父组件会顺带藏掉 layout 顶栏,做到整页全屏 -->
<button class="psp-act" type="button" :class="{ 'is-on': !controlsVisible }"
:title="controlsVisible ? '隐藏控制区' : '显示控制区'" @click.stop="toggleControls">
<EyeInvisibleOutlined v-if="controlsVisible" />
<EyeOutlined v-else />
</button>
<button class="psp-act" type="button" title="下载当前图片" @click.stop="onDownload">
<DownloadOutlined />
<span class="psp-badge" :class="{ 'is-on': downloaded }">{{ downloaded ? "" : "" }}</span>
@@ -177,8 +191,9 @@
</transition>
<!-- 精看:暂停时单击图片放大看。复用 ant-design 图片预览(自带缩放 / 旋转 / 翻转),
图片本体藏起来,预览层由组件自己渲染,不受舞台裁切影响 -->
<AImage :src="current?.url || ''" :style="{ display: 'none' }"
图片本体藏起来,预览层由组件自己渲染,不受舞台裁切影响。
这里用原图(full),舞台轮播放的是缩略图,精看要看清楚 -->
<AImage :src="currentFull" :style="{ display: 'none' }"
:preview="{ visible: picVisible, onVisibleChange: setPicVisible }" />
</div>
</template>
@@ -187,20 +202,24 @@
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { Image as AImage, message } from "ant-design-vue";
import { request } from "@/utils/request";
import { useVModel } from "@vueuse/core";
import {
DownOutlined, DownloadOutlined, CloseOutlined, PictureOutlined, EllipsisOutlined,
ThunderboltOutlined, RetweetOutlined, SwapOutlined, StepBackwardFilled,
StepForwardFilled, CaretRightFilled, PauseOutlined, UnorderedListOutlined,
EyeOutlined, EyeInvisibleOutlined,
} from "@ant-design/icons-vue";
/** 一张图片 */
export interface PhotoSlideItem {
/** 原图地址 */
/** 展示地址(舞台幻灯片 + 列表缩略图):一般给缩略图,轮播加载更快 */
url: string;
/** 缩略图地址(列表用,缺省用原图) */
/** 缩略图地址(列表用,缺省用 url) */
thumb?: string;
/** 名称(下载文件名) */
name?: string;
/** 原图地址:精看与下载用它,缺省退回 url */
full?: string;
}
const props = withDefaults(
@@ -208,10 +227,15 @@ const props = withDefaults(
items?: PhotoSlideItem[];
/** 每张停留秒数 */
interval?: number;
/** 底部控制区是否显示(页面可用它做「整页全屏」) */
controls?: boolean;
}>(),
{ items: () => [], interval: 5 }
{ items: () => [], interval: 5, controls: true }
);
const emit = defineEmits<{ (e: "close"): void }>();
const emit = defineEmits<{
(e: "close"): void;
(e: "update:controls", visible: boolean): void;
}>();
/* ---------------- 基础状态 ---------------- */
const items = computed(() => props.items || []);
@@ -225,12 +249,30 @@ const showList = ref(false);
const menu = ref<"" | "effect" | "more">("");
const downloaded = ref(false);
/**
* 底部控制区是否显示:由功能图标行的眼睛图标切换。
* 收起后舞台吃掉这部分空间,图片最大化;父组件可用 v-model:controls 拿到这个状态
* 去做整页全屏(例如 Preview.vue 藏掉 layout 顶部的两条栏)。
*
* passive: true 是关键:不传的话 useVModel 直接读写 props,
* 父组件没绑 v-model:controls 时内部改了值只会 emit 出去、props 不变,
* 按钮就"点了没反应"(与 VideoPlayer 一致)。
*/
const controlsVisible = useVModel(props, "controls", emit, { passive: true });
const toggleControls = () => {
menu.value = ""; // 顺手收起可能打开的弹层(特效 / 更多)
controlsVisible.value = !controlsVisible.value;
};
/** 舞台元素与像素尺寸:圆形格子的直径按舞台短边换算,避免非正方舞台被压成椭圆 */
const stageEl = ref<HTMLElement | null>(null);
const stageSize = ref({ w: 0, h: 0 });
let stageRo: ResizeObserver | null = null;
const current = computed<PhotoSlideItem | null>(() => items.value[index.value] ?? null);
/** 精看与下载用原图(full),没单独给就退回展示地址 */
const currentFull = computed(() => current.value?.full || current.value?.url || "");
const modeTitle = computed(
() => ({ list: "顺序循环", random: "随机播放", once: "播完停止" }[mode.value])
);
@@ -789,15 +831,30 @@ const picVisible = ref(false);
const setPicVisible = (v: boolean) => {
picVisible.value = v;
};
/** 单击舞台:播放中先暂停(原来的行为);已经暂停则打开精看,效果同 DmsFileEnquiry 的单图精看 */
/**
* 单击舞台:播放中先暂停(原来的行为);已经暂停则打开精看,效果同 DmsFileEnquiry 的单图精看。
*
* 页内全屏(控制区已收起)时只切暂停/播放:那会儿没有控制条,
* 再叠一层大图预览会盖住整屏、也没有恢复入口,容易把人"困住"。
*/
const onStageClick = () => {
if (!items.value.length) return;
if (!controlsVisible.value) {
toggle();
return;
}
if (isPlaying.value) {
toggle();
return;
}
setPicVisible(true);
};
/** 舞台提示:页内全屏时没有"看大图",文案跟着变,免得点了跟说好的不一样 */
const stageTitle = computed(() => {
if (!controlsVisible.value) return isPlaying.value ? "单击暂停" : "单击播放";
return isPlaying.value ? "单击暂停" : "单击看大图";
});
const playAt = (i: number) => {
index.value = i;
elapsed.value = 0;
@@ -848,13 +905,15 @@ const onDownload = async () => {
if (!it || downloading.value) return;
downloading.value = true;
try {
// 下载要原图,不是舞台上那张缩略图
const url = currentFull.value;
const res = await request<ArrayBuffer>({
url: it.url,
url,
method: "get",
responseType: "arraybuffer",
});
const blobUrl = window.URL.createObjectURL(new Blob([res]));
const ext = (it.url.match(/\.(\w{2,5})(?:\?|#|$)/) || [])[1] || "jpg";
const ext = (url.match(/\.(\w{2,5})(?:\?|#|$)/) || [])[1] || "jpg";
const a = document.createElement("a");
a.href = blobUrl;
a.download = `${it.name || "image"}.${ext}`;
@@ -963,11 +1022,37 @@ onBeforeUnmount(() => {
cursor: pointer;
}
/* 暂停后单击就是精看,光标跟着变 */
.psp-stage.is-paused {
/* 暂停后单击就是精看,光标跟着变;
页内全屏(.is-immersive)时单击是继续播放,不给放大镜光标,免得误导 */
.psp-stage.is-paused:not(.is-immersive) {
cursor: zoom-in;
}
/* 控制区收起后的恢复入口(整页用法没有弹窗右上角按钮) */
.psp-restore {
position: absolute;
top: 14px;
right: 14px;
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
font-size: 18px;
color: #fff;
background: rgba(0, 0, 0, .38);
border: 0;
border-radius: 10px;
cursor: pointer;
opacity: .55;
transition: opacity .2s;
}
.psp-restore:hover {
opacity: 1;
}
.psp-img {
position: absolute;
inset: 0;
+14
View File
@@ -35,9 +35,23 @@ async function setupApp() {
// 初始化 i18n
await setupI18n(app);
setupShareImage();
app.mount("#app");
}
/**
* 分享卡片(微信收藏/转发等)的缩略图要求绝对地址,相对路径会被忽略,
* 这里按当前域名把 index.html 里的 og:image 补全。
*/
function setupShareImage() {
const meta = document.querySelector('meta[property="og:image"]');
const content = meta?.getAttribute("content");
if (meta && content && !/^https?:\/\//i.test(content)) {
meta.setAttribute("content", new URL(content, location.origin).href);
}
}
setupPlugins();
setupApp();
+8 -4
View File
@@ -110,7 +110,9 @@ export async function setupRouter(app: App) {
),
props: true,
meta: {
title: "首页",
// 这个 title 会被 App.vue 写进 document.title,
// 微信「收藏 / 分享」卡片的第一行取的就是它,所以用品牌名而不是"首页"
title: "有家相册",
},
});
router.addRoute({
@@ -129,17 +131,19 @@ export async function setupRouter(app: App) {
name: "dashboard",
component: () => import("../layout/home.vue"),
meta: {
title: "首页",
// 这里不要用 description:App.vue 会优先把 meta.description 写进 document.title,
// 而微信「收藏 / 转发」卡片的第一行取的就是 document.title,必须是品牌名,
// 否则分享出去显示的是"欢迎页"
title: "有家相册",
role: "*",
description: "欢迎页",
},
children: [
{
path: "",
name: "dashboardWelcome",
meta: {
title: "有家相册",
role: "*",
description: "欢迎页",
},
component: () => import("../views/dashboard.vue"),
},
@@ -3,7 +3,7 @@ import { request,download } from "@/utils/request";
import type { PublishPhotoData } from "@/data/auto/publishPhotoData";
import type { PublishPhotoSearchCriteria } from "@/data/auto/publishPhotoSearchCriteria";
import type { PublishPhotoResultData } from "@/data/auto/publishPhotoResultData";
import type { PublishPhotoDetailEntity } from "@/data/auto/publishPhotoDetailEntity";
import type { PublishPhotoDetailDisplayData } from "@/data/auto/publishPhotoDetailDisplayData";
import type { PublishData } from "@/data/auto/publishData";
class PublishPhotoService{
@@ -70,7 +70,7 @@ class PublishPhotoService{
};
async FindPublishPhotoDetailByPublishName(data: string) {
return request<PublishPhotoDetailEntity[]>(
return request<PublishPhotoDetailDisplayData[]>(
{
url: "/PublishPhoto/FindPublishPhotoDetailByPublishName?publishName="+data,
method: "post",
@@ -80,7 +80,7 @@ class PublishPhotoService{
};
async FindPublishPhotoDetail() {
return request<PublishPhotoDetailEntity[]>(
return request<PublishPhotoDetailDisplayData[]>(
{
url: "/PublishPhoto/FindPublishPhotoDetail",
method: "post",
+23
View File
@@ -23,4 +23,27 @@ select[readonly]:not([disabled])+.dropdown-toggle.custom-selectpicker,
input[type=checkbox][readonly]:not([disabled]),
input[type=radio][readonly]:not([disabled]) {
background-color: #f3f3f3 !important;
}
/* 页面级「整页全屏」(沉浸看片/看图):
body 上有 .pv-fullscreen 时才生效,该 class 由影视厅 PreviewVideo.vue、
图片预览 Preview.vue 在收起控制区时临时挂上,离开页面立即移除,所以不会影响其它页面。
1. 藏掉 layout 顶部两条栏(标题栏 + 页面头);
2. 内容区与页面容器撑到整屏,去掉原有的 8px 内边距,做到真正的全屏铺满。
容器高度是行内样式(bsInfo.height - 120)、Preview 还用了负外边距,只能用 !important 覆盖。 */
body.pv-fullscreen .layout-header,
body.pv-fullscreen .layout-page-header {
display: none !important;
}
body.pv-fullscreen .layout-content-view {
height: 100vh !important;
padding: 0 !important;
}
body.pv-fullscreen .pv-page {
width: 100% !important;
height: 100vh !important;
padding: 0 !important;
margin: 0 !important;
}
+34 -17
View File
@@ -1,14 +1,14 @@
<template>
<div class="pv-page">
<PhotoSlidePlayer :items="slides" @close="onClose" />
<PhotoSlidePlayer v-model:controls="controlsVisible" :items="slides" @close="onClose" />
</div>
</template>
<script lang="ts" setup>
import { watch, computed, inject, reactive, ref, VNode, h, unref, onMounted, onBeforeMount } from "vue";
import { watch, computed, inject, reactive, ref, VNode, h, unref, onMounted, onBeforeMount, onUnmounted } from "vue";
import { useRoute, useRouter } from 'vue-router'
import PhotoSlidePlayer from "@/components/basic/PhotoSlidePlayer.vue";
import type { PhotoSlideItem } from "@/components/basic/PhotoSlidePlayer.vue";
import { PublishPhotoDetailEntity } from "@/data/auto/publishPhotoDetailEntity";
import { PublishPhotoDetailDisplayData } from "@/data/auto/publishPhotoDetailDisplayData";
import publishPhotoService from "@/service/master/publishPhotoService";
import { bsInfo } from "@/utils/browser"
import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria";
@@ -20,7 +20,13 @@ import dmsFileService from "@/service/master/dmsFileService";
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons-vue';
import { SearchCriteriaResultData } from "@/data/auto/searchCriteriaResultData";
const images = ref<PublishPhotoDetailEntity[]>([]);
/**
* 后端 PublishPhotoService 会把同一张图给出两个地址(见 PublishPhotoService.auto.cs):
* FileName —— 原图
* FormattedPic —— 缩放后的缩略图
* 幻灯片轮播与图片列表用缩略图(加载快),精看和下载用原图。
*/
const images = ref<PublishPhotoDetailDisplayData[]>([]);
const route = useRoute();
@@ -30,6 +36,22 @@ const router = useRouter();
const publishName = ref<string>(route.params.id as string)
const mode = ref<string>(route.params.mode as string)
/**
* 控制区显隐:与影视厅 PreviewVideo.vue 同一套机制——
* 收起控制区时给 body 挂 .pv-fullscreen,藏掉 layout 顶部的两条栏
* (标题栏 + 页面头)并把内容区撑到整屏,图片真正占满全屏。
* class 挂在 body 上,离开本页立即移除,不会影响其它页面(样式见 styles/main.css)。
*/
const controlsVisible = ref(true);
watch(controlsVisible, (visible) => {
document.body.classList.toggle("pv-fullscreen", !visible);
});
onUnmounted(() => {
document.body.classList.remove("pv-fullscreen");
});
const toIndex = ref<number>(0)
const beforeChange = (from: number, to: number) => {
if (to > 0) {
@@ -37,11 +59,12 @@ const beforeChange = (from: number, to: number) => {
}
}
// 交给 PhotoSlidePlayer 播放:FileName 为缩略图,PublishName 为原图
// 交给 PhotoSlidePlayer 播放:舞台与列表用 FormattedPic(缩略图),精看/下载用 FileName(原图)
const slides = computed<PhotoSlideItem[]>(() =>
images.value.map((item) => ({
url: item.PublishName,
thumb: item.FileName,
url: item.FormattedPic || item.FileName,
thumb: item.FormattedPic || item.FileName,
full: item.FileName || item.FormattedPic,
name: (item.FileName || "").replace(/\.[^./\\]+$/, "") || undefined,
}))
);
@@ -69,17 +92,11 @@ onMounted(async () => {
}
else if (publishName.value && publishName.value.length > 0) {
publishPhotoService.FindPublishPhotoDetailByPublishName(publishName.value).then(res => {
for (let i = 0; i <res.length; i++) {
res[i].PublishName = res[i].FileName;
}
images.value = res;
});
}
else {
publishPhotoService.FindPublishPhotoDetail().then(res => {
for (let i = 0; i <res.length; i++) {
res[i].PublishName = res[i].FileName;
}
images.value = res;
});
}
@@ -110,12 +127,12 @@ const findData = async (params: number) => {
if (dr && dr.Datas && dr.Datas.length > 0) {
images.value = [];
let detail: PublishPhotoDetailEntity;
let detail: PublishPhotoDetailDisplayData;
for (let i = 0; i < dr.Datas.length; i++) {
detail = new PublishPhotoDetailEntity()
detail.FileName = dr.Datas[i].FormattedPic;
detail = new PublishPhotoDetailDisplayData()
detail.FileName = dr.Datas[i].Url; // 原图:精看 / 下载
detail.FormattedPic = dr.Datas[i].FormattedPic; // 缩略图:舞台轮播 / 列表
detail.Notice = dr.Datas[i].LandscapeIndc;
detail.PublishName = dr.Datas[i].Url;
images.value.push(detail);
}
}
-24
View File
@@ -130,27 +130,3 @@ const loadVideos = async () => {
}
</style>
<style>
/* 注意:这里是全局样式,但所有选择器都要求 body 上有 .pv-fullscreen
(只在影视厅收起控制区时由 PreviewVideo.vue 临时挂上,离开页面即移除),
所以不会影响其它页面。
收起控制区 = 页面全屏:
1. 藏掉 layout 顶部两条栏(标题栏 64px + 页面头 42px);
2. 内容区和视频容器撑到整屏,去掉原有的 8px 内边距,做到真正的全屏铺满。
容器高度是行内样式(bsInfo.height - 100/-120),只能用 !important 覆盖。 */
body.pv-fullscreen .layout-header,
body.pv-fullscreen .layout-page-header {
display: none !important;
}
body.pv-fullscreen .layout-content-view {
height: 100vh !important;
padding: 0 !important;
}
body.pv-fullscreen .pv-page {
height: 100vh !important;
padding-right: 0 !important;
}
</style>