feat(图片页 1/2): 详细信息与移动到相册改用居中卡片弹窗、二级菜单返回键加空隙
#1 「详细信息」「移动到相册」弃用右侧抽屉,改复用 x-modal 的居中卡片弹窗(新增 #image-detail-modal / #image-movements-modal 及其内容容器,三个新模板): - 详细信息:顶部小缩略图 + dl 行式布局(<640px 自动上下堆叠),标签 13px / 值 14px / leading-6;字段顺序按老师要求 = 上传时间 → 图片名称 → 其余 10 项(原抽屉里 12 项一个没删, 只换位置);弹窗内不放复制链接/下载/删除(单一职责)。 - 移动到相册:单选列表(当前所在相册带「当前」徽标)、行 min-h-[44px](手机点击区)、 未选中时「移动」disabled;请求逐字沿用原实现(同 route user.images.movement、同 payload); 点「移动」才提交,成功关弹窗 + resetImages + toastr。 - 右侧抽屉保留本职(工具栏「相册列表」入口、自带无限加载、相册增删改查)原样不动; 桌面右键、手机长按、工具栏三个入口都走新弹窗。 #2 二级菜单「<返回」箭头与文字间距:context-js.less 加 7px(选择器权重压过标记自带 mr-1)。 关键:本轮的 images.blade.php 改动不属于补丁文件(context-js.js 两份 md5 全程 = Dockerfile 钉住的 b916555a…,未动),Dockerfile 的 images_blade_md5 已随自证同步更新为 27bb3f20…,补丁重生成 2563 行 且「打回上游 = 仓库文件」字节一致。 验证:四套回归我自己跑过 —— longpress 79/79、sidebar 6/6、images-modal 35/35(静态契约: 12 字段与顺序 / 13-14px 字号 / min-h-44 / 请求不变 / 无硬编码色 / 返回键 7px / 补丁 md5 未变)、 images-modal-dom 35/35(jsdom 真跑页面内联脚本:点菜单 → 内容进新弹窗、抽屉全程为空且未被推开、 单选互斥、点「移动」实发 {selected,id,album_id} 后关弹窗、抽屉路径仍能筛选相册); php -l + view:cache exit 0;重建产物含 text-[13px]/[14px]/min-h-[44px]/max-h-[50vh]/ls-badge 等。
This commit is contained in:
1 parent
1a57f7660c
commit
30862d602f
8 files changed
+987
-132
No files matched your search
+2
-2
@@ -109,7 +109,7 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \
|
||||
RUN printf '%s\n' \
|
||||
'b916555ab122d2b3db99153d9316a887 ./public/js/context-js/context-js.js' \
|
||||
'b916555ab122d2b3db99153d9316a887 ./resources/js/context-js.js' \
|
||||
'5da6f22c67fc824d5af88a7de32ad7f6 ./resources/views/user/images.blade.php' \
|
||||
'27bb3f203c931633961798dda7aab7dc ./resources/views/user/images.blade.php' \
|
||||
| md5sum -c - \
|
||||
&& grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \
|
||||
&& grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \
|
||||
@@ -130,7 +130,7 @@ RUN printf '%s\n' \
|
||||
"fork_sha=${FORK_SHA}" \
|
||||
"lsky_commit=${LSKY_COMMIT}" \
|
||||
"context_js_md5=b916555ab122d2b3db99153d9316a887" \
|
||||
"images_blade_md5=5da6f22c67fc824d5af88a7de32ad7f6" \
|
||||
"images_blade_md5=27bb3f203c931633961798dda7aab7dc" \
|
||||
> .code-revision \
|
||||
&& cat .code-revision
|
||||
|
||||
|
||||
+290
-62
@@ -36,7 +36,7 @@
|
||||
# 三个文件各自的 md5(与 Dockerfile「自证 2」里的期望值一致):
|
||||
# b916555ab122d2b3db99153d9316a887 src/resources/js/context-js.js
|
||||
# b916555ab122d2b3db99153d9316a887 src/public/js/context-js/context-js.js
|
||||
# 5da6f22c67fc824d5af88a7de32ad7f6 src/resources/views/user/images.blade.php
|
||||
# 27bb3f203c931633961798dda7aab7dc src/resources/views/user/images.blade.php
|
||||
#
|
||||
# 上游原文件 md5(改动前,供比对):
|
||||
# c8e57f6232848ca8277341ddf1a3a7a6 resources/js/context-js.js
|
||||
@@ -2075,7 +2075,7 @@
|
||||
</a>
|
||||
</x-slot>
|
||||
|
||||
@@ -76,17 +79,26 @@
|
||||
@@ -76,33 +79,56 @@
|
||||
</x-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2108,7 +2108,20 @@
|
||||
</div>
|
||||
<div id="drawer-content" class="overflow-y-auto"></div>
|
||||
</div>
|
||||
@@ -96,13 +108,16 @@
|
||||
</div>
|
||||
|
||||
+ {{-- 图片「详细信息」与「移动到相册」改用居中卡片弹窗(复用 components/modal.blade.php,
|
||||
+ 手机上它就是「底部抽屉式」——弹窗容器在 <640px 时贴底、不依赖 hover)。
|
||||
+ 右侧抽屉 #drawer 保留它原来的本职:顶部工具栏的「相册列表」入口照旧走抽屉。 --}}
|
||||
+ <x-modal id="image-detail-modal">
|
||||
+ <div id="image-detail-content"></div>
|
||||
+ </x-modal>
|
||||
+
|
||||
+ <x-modal id="image-movements-modal">
|
||||
+ <div id="image-movements-content"></div>
|
||||
+ </x-modal>
|
||||
+
|
||||
<script type="text/html" id="images-item-tpl">
|
||||
<a href="javascript:void(0)" data-id="__id__" data-json='__json__' class="images-item relative cursor-default rounded outline outline-2 outline-offset-2 outline-transparent">
|
||||
<div class="image-selector absolute z-[2] top-0 right-0 overflow-hidden cursor-pointer sm:hidden group-hover:block">
|
||||
<div class="p-1 text-xl sm:text-2xl">
|
||||
@@ -2128,7 +2141,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<img alt="__name__" data-original="__url__" src="__thumb_url__" width="__width__" height="__height__">
|
||||
@@ -114,22 +129,22 @@
|
||||
@@ -114,22 +140,22 @@
|
||||
<div id="album-add" class="flex flex-col w-full hidden border rounded p-2">
|
||||
<p class="error-message text-white p-2 mb-2 text-sm bg-red-500 rounded hidden"></p>
|
||||
<form class="w-full space-y-2" action="/user/albums">
|
||||
@@ -2158,7 +2171,7 @@
|
||||
</a>
|
||||
</script>
|
||||
|
||||
@@ -137,9 +152,9 @@
|
||||
@@ -137,71 +163,106 @@
|
||||
<div id="album-edit" data-id="__id__" class="flex flex-col w-full border rounded p-2">
|
||||
<p class="error-message text-white p-2 mb-2 text-sm bg-red-500 rounded hidden"></p>
|
||||
<form class="w-full space-y-2" action="/user/albums/__id__">
|
||||
@@ -2171,71 +2184,143 @@
|
||||
</form>
|
||||
</div>
|
||||
</script>
|
||||
@@ -148,51 +163,51 @@
|
||||
<div class="my-4 px-4 space-y-3">
|
||||
<div>
|
||||
<span class="text-sm font-semibold">相册名称</span>
|
||||
|
||||
+ {{-- 图片「详细信息」:居中卡片弹窗(不再渲染进右侧抽屉)。
|
||||
+ 字段顺序按老师要求:**上传时间第一、图片名称紧跟其后**,其余字段一个没删、只换了位置
|
||||
+ (相册名称 / 使用策略 / 图片原始名称 / 图片大小 / 图片类型 / 尺寸 / MD5 / SHA-128 / 权限 / 上传 IP)。
|
||||
+ 排版:标签在左、值在右(窄屏 flex-col 自动上下堆叠);标签 13px、值 14px,行高 leading-6 宽松一些。
|
||||
+ 顶部一张小缩略图。这里不放任何快捷操作(复制链接/下载/删除都不进这个弹窗)。 --}}
|
||||
<script type="text/html" id="image-detail-tpl">
|
||||
- <div class="my-4 px-4 space-y-3">
|
||||
- <div>
|
||||
- <span class="text-sm font-semibold">相册名称</span>
|
||||
- <p class="my-2 break-words text-gray-700">__album_name__</p>
|
||||
+ <p class="my-2 break-words text-ink">__album_name__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">使用策略</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">使用策略</div>
|
||||
- <p class="my-2 break-words text-gray-600">__strategy_name__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__strategy_name__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片名称</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">图片名称</div>
|
||||
- <p class="my-2 break-words text-gray-600">__filename__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__filename__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片原始名称</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">图片原始名称</div>
|
||||
- <p class="my-2 break-words text-gray-600">__origin_name__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__origin_name__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片大小</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">图片大小</div>
|
||||
- <p class="my-2 break-words text-gray-600">__size__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__size__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片类型</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">图片类型</div>
|
||||
- <p class="my-2 break-words text-gray-600">__mimetype__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__mimetype__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">尺寸</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">尺寸</div>
|
||||
- <p class="my-2 break-words text-gray-600">__width__ * __height__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__width__ * __height__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">MD5</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">MD5</div>
|
||||
- <p class="my-2 break-words text-gray-600">__md5__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__md5__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">SHA-128</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">SHA-128</div>
|
||||
- <p class="my-2 break-words text-gray-600">__sha1__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__sha1__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">权限</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">权限</div>
|
||||
- <p class="my-2 break-words text-gray-600">__permission__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__permission__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">上传 IP</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">上传 IP</div>
|
||||
- <p class="my-2 break-words text-gray-600">__uploaded_ip__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__uploaded_ip__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">上传时间</div>
|
||||
- </div>
|
||||
- <div>
|
||||
- <div class="text-sm font-semibold">上传时间</div>
|
||||
- <p class="my-2 break-words text-gray-600">__created_at__</p>
|
||||
+ <p class="my-2 break-words text-ink-2">__created_at__</p>
|
||||
+ <div class="mx-auto w-full max-w-2xl">
|
||||
+ <img src="__thumb_url__" alt="__filename__" class="mb-4 h-20 w-20 rounded-lg border border-line bg-surface-2 object-cover">
|
||||
+ <dl class="space-y-3">
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">上传时间</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__created_at__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片名称</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__filename__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">相册名称</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__album_name__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">使用策略</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__strategy_name__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片原始名称</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__origin_name__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片大小</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__size__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片类型</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__mimetype__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">尺寸</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__width__ * __height__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">MD5</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__md5__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">SHA-128</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__sha1__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">权限</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__permission__</dd>
|
||||
+ </div>
|
||||
+ <div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
+ <dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">上传 IP</dt>
|
||||
+ <dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__uploaded_ip__</dd>
|
||||
+ </div>
|
||||
+ </dl>
|
||||
+ </div>
|
||||
+ </script>
|
||||
+
|
||||
+ {{-- 「移动到相册」弹窗:相册列表(单选)+ 底部「移动」「取消」。
|
||||
+ 列表数据与顶部工具栏的「相册列表」同一个接口;点一行只选中,底部「移动」才提交。
|
||||
+ 每行 min-h-[44px](手机点击区 ≥44px),当前所在相册带「当前」标记(__current_badge__)。 --}}
|
||||
+ <script type="text/html" id="movements-container-tpl">
|
||||
+ <div class="mx-auto flex w-full max-w-xl flex-col">
|
||||
+ <p class="text-[15px] font-semibold text-ink">移动到相册</p>
|
||||
+ <p class="mt-0.5 mb-3 text-[13px] text-ink-3">已选择 __count__ 张图片</p>
|
||||
+ <div id="movements-albums" class="flex max-h-[50vh] w-full flex-col space-y-1 overflow-y-auto pr-1"></div>
|
||||
+ <div class="mt-4 flex justify-end gap-2 border-t border-line pt-3">
|
||||
+ <button type="button" id="movements-cancel" class="ls-btn h-11 px-4 sm:h-9">取消</button>
|
||||
+ <button type="button" id="movements-confirm" class="ls-btn ls-btn-primary h-11 px-4 sm:h-9" disabled>移动</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
@@ -201,7 +216,8 @@
|
||||
|
||||
+ {{-- 相册列表行:整行是一个点击区(min-h-[44px]),不用 span(避免被无限加载的
|
||||
+ "点 span 加载更多"委托命中)。选中态由 JS 切换 border-brand/bg-brand-soft/text-brand。 --}}
|
||||
+ <script type="text/html" id="movements-album-item-tpl">
|
||||
+ <a href="javascript:void(0)" data-id="__id__" data-selected="false" class="movements-album flex min-h-[44px] w-full items-center gap-2 rounded-lg border border-line bg-surface-2 px-3 py-2 text-ink hover:bg-surface-3">
|
||||
+ <i class="selected-mark fas fa-check-circle w-4 shrink-0 text-brand opacity-0" aria-hidden="true"></i>
|
||||
+ <div class="min-w-0 flex-1 truncate text-[14px]">__name__</div>
|
||||
+ __current_badge__
|
||||
+ <div class="shrink-0 text-[13px] text-ink-3">__image_num__</div>
|
||||
+ </a>
|
||||
+ </script>
|
||||
+
|
||||
@push('scripts')
|
||||
<script src="{{ asset('js/justified-gallery/jquery.justifiedGallery.min.js') }}"></script>
|
||||
<script src="{{ asset('js/viewer-js/viewer.min.js') }}"></script>
|
||||
<script src="{{ asset('js/dragselect/ds.min.js') }}"></script>
|
||||
@@ -2245,7 +2330,19 @@
|
||||
<script src="{{ asset('js/clipboard/index.browser.js') }}"></script>
|
||||
<script src="{{ asset('js/clipboard/clipboard.min.js') }}"></script>
|
||||
<script>
|
||||
@@ -255,6 +271,7 @@
|
||||
@@ -225,6 +286,11 @@
|
||||
const $photos = $(IMAGES_GRID);
|
||||
const $drawer = $("#drawer");
|
||||
const $drawerMask = $('#drawer-mask');
|
||||
+ // 居中卡片弹窗(复用 components/modal.blade.php 的 Alpine store,用法同 admin 页)
|
||||
+ const modal = Alpine.store('modal');
|
||||
+ // 「移动到相册」弹窗里的相册列表容器 / 底部按钮
|
||||
+ const MOVEMENTS_MODAL = 'image-movements-modal';
|
||||
+ const DETAIL_MODAL = 'image-detail-modal';
|
||||
const viewer = new Viewer(document.getElementById('images-grid'), {url: 'data-original'});
|
||||
const drawer = {
|
||||
open(title, content, callback) {
|
||||
@@ -255,6 +321,7 @@
|
||||
const imagesInfinite = utils.infiniteScroll(IMAGES_SCROLL, {
|
||||
url: '{{ route('user.images') }}',
|
||||
classes: ['dragselect'],
|
||||
@@ -2253,7 +2350,7 @@
|
||||
success: function (response) {
|
||||
if (!response.status) {
|
||||
return toastr.error(response.message);
|
||||
@@ -308,7 +325,7 @@
|
||||
@@ -308,7 +375,7 @@
|
||||
}
|
||||
|
||||
const getAlbums = (options, callback) => {
|
||||
@@ -2262,7 +2359,7 @@
|
||||
let content = $('#albums-container-tpl').html();
|
||||
drawer.toggle(title, content, function () {
|
||||
let $albums = $('#albums-container');
|
||||
@@ -337,8 +354,8 @@
|
||||
@@ -337,8 +404,8 @@
|
||||
if (albums[i].id === selectedAlbum.id) {
|
||||
// 选中的相册高亮
|
||||
item = item
|
||||
@@ -2273,7 +2370,7 @@
|
||||
}
|
||||
|
||||
html += item;
|
||||
@@ -486,10 +503,10 @@
|
||||
@@ -486,10 +553,10 @@
|
||||
operates = ['refresh'];
|
||||
}
|
||||
if (selected.length === 1) {
|
||||
@@ -2286,7 +2383,7 @@
|
||||
}
|
||||
if (selected.length && selectedAlbum.id !== undefined) {
|
||||
operates.push('remove');
|
||||
@@ -509,6 +526,26 @@
|
||||
@@ -509,6 +576,26 @@
|
||||
ds.subscribe('elementselect', _ => bindOperates());
|
||||
ds.subscribe('elementunselect', _ => bindOperates());
|
||||
|
||||
@@ -2313,7 +2410,119 @@
|
||||
$photos.on('click', '.image-selector', function () {
|
||||
ds.toggleSelection($(this).closest('a'));
|
||||
bindOperates();
|
||||
@@ -575,7 +612,7 @@
|
||||
@@ -535,25 +622,94 @@
|
||||
|
||||
const methods = {
|
||||
movements() {
|
||||
- getAlbums({title: '选择相册'}, e => {
|
||||
- let selected = ds.getSelection().map(item => $(item).data('id'));
|
||||
- $headerTitle.text(`移动 ${selected.length} 张图片至...`)
|
||||
- $(e).off('click', '>a').on('click', '>a', function () {
|
||||
- axios.put('{{ route('user.images.movement') }}', {
|
||||
- selected: selected,
|
||||
- id: $(this).data('id'),
|
||||
- album_id: selectedAlbum.id || 0,
|
||||
- }).then(response => {
|
||||
- if (response.data.status) {
|
||||
- drawer.close();
|
||||
- resetImages();
|
||||
- toastr.success(response.data.message);
|
||||
- } else {
|
||||
- toastr.warning(response.data.message);
|
||||
- }
|
||||
- })
|
||||
+ // 「移动到相册」改用居中卡片弹窗(不再渲染进右侧抽屉 ——
|
||||
+ // 抽屉只留给顶部工具栏的「相册列表」入口)。
|
||||
+ // 相册列表仍走同一个 user.albums 接口 + 同一个 utils.infiniteScroll;
|
||||
+ // 弹窗内是单选列表(当前所在相册带「当前」标记),底部「移动」「取消」。
|
||||
+ let selected = ds.getSelection().map(item => $(item).data('id'));
|
||||
+ if (! selected.length) {
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ $('#image-movements-content').html(
|
||||
+ $('#movements-container-tpl').html().replace(/__count__/g, selected.length)
|
||||
+ );
|
||||
+
|
||||
+ const $movementsAlbums = $('#movements-albums');
|
||||
+ const $movementsConfirm = $('#movements-confirm');
|
||||
+ let targetId = null; // 本次要移动到的相册(单选)
|
||||
+
|
||||
+ // 单选:点一行只选中并高亮,不发请求;底部「移动」才提交
|
||||
+ const selectRow = ($row) => {
|
||||
+ targetId = $row.length ? $row.data('id') : null;
|
||||
+ $movementsAlbums.find('.movements-album').each(function () {
|
||||
+ let on = targetId !== null && $(this).data('id') === targetId;
|
||||
+ $(this)
|
||||
+ .toggleClass('border-brand bg-brand-soft text-brand', on)
|
||||
+ .toggleClass('border-line bg-surface-2 text-ink', ! on)
|
||||
+ .attr('data-selected', on ? 'true' : 'false')
|
||||
+ .find('.selected-mark').toggleClass('opacity-0', ! on);
|
||||
});
|
||||
+ $movementsConfirm.prop('disabled', targetId === null);
|
||||
+ };
|
||||
+
|
||||
+ utils.infiniteScroll('#movements-albums', {
|
||||
+ url: '{{ route('user.albums') }}',
|
||||
+ success: function (response) {
|
||||
+ if (! response.status) {
|
||||
+ return toastr.error(response.message);
|
||||
+ }
|
||||
+
|
||||
+ let albums = response.data.albums.data;
|
||||
+ if (albums.length <= 0 || response.data.albums.current_page === response.data.albums.last_page) {
|
||||
+ this.finished = true;
|
||||
+ }
|
||||
+
|
||||
+ let html = '';
|
||||
+ for (const i in albums) {
|
||||
+ html += $('#movements-album-item-tpl').html()
|
||||
+ .replace(/__id__/g, albums[i].id)
|
||||
+ .replace(/__name__/g, albums[i].name)
|
||||
+ .replace(/__image_num__/g, albums[i].image_num)
|
||||
+ // 当前所在相册标出来
|
||||
+ .replace(/__current_badge__/g, albums[i].id === selectedAlbum.id
|
||||
+ ? '<div class="ls-badge shrink-0 bg-brand-soft text-brand">当前</div>'
|
||||
+ : '')
|
||||
+ }
|
||||
+
|
||||
+ $movementsAlbums.append(html);
|
||||
+ }
|
||||
});
|
||||
+
|
||||
+ $movementsAlbums.off('click', '.movements-album').on('click', '.movements-album', function () {
|
||||
+ selectRow($(this));
|
||||
+ });
|
||||
+
|
||||
+ $('#movements-cancel').off('click').on('click', _ => modal.close(MOVEMENTS_MODAL));
|
||||
+
|
||||
+ // 移动请求沿用原实现(同一个 route、同一份 payload),
|
||||
+ // 唯一区别:原来「点哪行就移动哪行」,现在是「选中后点移动」
|
||||
+ $movementsConfirm.off('click').on('click', function () {
|
||||
+ if (targetId === null) {
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ axios.put('{{ route('user.images.movement') }}', {
|
||||
+ selected: selected,
|
||||
+ id: targetId,
|
||||
+ album_id: selectedAlbum.id || 0,
|
||||
+ }).then(response => {
|
||||
+ if (response.data.status) {
|
||||
+ modal.close(MOVEMENTS_MODAL);
|
||||
+ resetImages();
|
||||
+ toastr.success(response.data.message);
|
||||
+ } else {
|
||||
+ toastr.warning(response.data.message);
|
||||
+ }
|
||||
+ })
|
||||
+ });
|
||||
+
|
||||
+ modal.open(MOVEMENTS_MODAL);
|
||||
},
|
||||
remove() {
|
||||
let selected = ds.getSelection().map(item => $(item).data('id'));
|
||||
@@ -575,7 +731,7 @@
|
||||
permission() {
|
||||
Swal.fire({
|
||||
title: '选择一个权限',
|
||||
@@ -2322,7 +2531,26 @@
|
||||
input: 'select',
|
||||
inputOptions: {
|
||||
public: '公开',
|
||||
@@ -853,6 +890,10 @@
|
||||
@@ -684,6 +840,7 @@
|
||||
if (response.data.status) {
|
||||
let image = response.data.data.image;
|
||||
let content = $('#image-detail-tpl').html()
|
||||
+ .replace(/__thumb_url__/g, image.thumb_url)
|
||||
.replace(/__album_name__/g, image.album ? image.album.name : '-')
|
||||
.replace(/__strategy_name__/g, image.strategy ? image.strategy.name : '-')
|
||||
.replace(/__filename__/g, image.filename.replace(/\$/g, '$$$$'))
|
||||
@@ -697,7 +854,9 @@
|
||||
.replace(/__permission__/g, image.permission === 1 ? '公开' : '私有')
|
||||
.replace(/__uploaded_ip__/g, image.uploaded_ip)
|
||||
.replace(/__created_at__/g, image.created_at)
|
||||
- drawer.open(item.filename, content);
|
||||
+ // 「详细信息」改成居中卡片弹窗(原来画进右侧抽屉)
|
||||
+ $('#image-detail-content').html(content);
|
||||
+ modal.open(DETAIL_MODAL);
|
||||
} else {
|
||||
toastr.error(response.data.message);
|
||||
}
|
||||
@@ -853,6 +1012,10 @@
|
||||
case 'delete': // 删除
|
||||
methods.delete();
|
||||
break;
|
||||
|
||||
@@ -1 +1 @@
|
||||
/*! tailwindcss v3.0.23 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Noto Sans SC,ui-sans-serif,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{-webkit-print-color-adjust:exact;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;color-adjust:exact;padding-right:2.5rem}[multiple]{-webkit-print-color-adjust:unset;background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;color-adjust:unset;padding-right:.75rem}[type=checkbox],[type=radio]{-webkit-print-color-adjust:exact;--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;color-adjust:exact;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-usLine truncated
|
||||
/*! tailwindcss v3.0.23 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Noto Sans SC,ui-sans-serif,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{-webkit-print-color-adjust:exact;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;color-adjust:exact;padding-right:2.5rem}[multiple]{-webkit-print-color-adjust:unset;background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;color-adjust:unset;padding-right:.75rem}[type=checkbox],[type=radio]{-webkit-print-color-adjust:exact;--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;color-adjust:exact;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-usLine truncated
|
||||
@@ -1 +1 @@
|
||||
.nav-header{color:var(--lsky-text-3);display:block;font-size:12.5px;font-weight:700;line-height:20px;padding:3px 15px;text-shadow:none;text-transform:uppercase}.dropdown-menu{background-clip:padding-box;background-color:var(--lsky-surface);border:0;border-radius:6px;box-shadow:var(--lsky-shadow-pop);display:none;float:left;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;left:0;list-style:none;margin:2px 0 0;min-width:160px;padding:5px 0;position:absolute;text-align:left;top:100%;z-index:1000}.dropdown-menu.pull-right{left:auto;right:0}.dropdown-menu .divider{background-color:var(--lsky-surface-3);border-bottom:1px solid var(--lsky-border);height:1px;margin:9px 1px;overflow:hidden}.dropdown-menu a{clear:both;color:var(--lsky-text);display:block;font-weight:400;line-height:20px;padding:5px 20px;text-decoration:none;white-space:nowrap}.dropdown-menu .disabled>a:hover{background-color:transparent;cursor:default;text-decoration:none}.dropdown-menu .active>a,.dropdown-menu .active>a:hover,.dropdown-menu .dropdown-submenu:hover>a,.dropdown-menu li>a:focus,.dropdown-menu li>a:hover{background-color:var(--lsky-accent);background-image:none;background-repeat:repeat-x;color:#fff;text-decoration:none}.dropdown-menu .active>a,.dropdown-menu .active>a:hover{outline:0}.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:var(--lsky-text-3)}.dropdown-menu .open{*z-index:1000}.dropdown-menu .open>.dropdown-menu{display:block}.dropdown-menu .pull-right>.dropdown-menu{left:auto;right:0}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-bottom:4px solid var(--lsky-border-2);border-top:0;content:"\2191"}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{bottom:100%;margin-bottom:1px;top:auto}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{border-radius:0 6px 6px 6px;left:100%;margin-left:-1px;margin-top:-6px;top:0}.dropdown-submenu>.dropdown-menu.drop-left{left:-100%}.dropdown-submenu:hover .dropdown-menu{display:block}.dropdown-submenu:hover>a:after{border-left-color:var(--lsky-surface)}.dropdown-submenu>a:after{border-bottom:5px solid transparent;border-left:5px solid transparent;border-left-color:var(--lsky-border-2);border-right:0 solid transparent;border-top:5px solid transparent;content:" ";display:block;float:right;height:0;margin-right:-10px;margin-top:5px;width:0}.dropdown .dropdown-menu .nav-header{padding-left:20px;padding-right:20px}.dropdown-context .nav-header{cursor:default}.dropdown-context .dropdown-submenu:hover .dropdown-menu{display:none}.dropdown-context .dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-context-up:before,.dropdown-context:before{border-bottom:7px solid rgba(0,0,0,.2);border-left:7px solid transparent;border-right:7px solid transparent;content:"";display:inline-block;left:9px;position:absolute;top:-7px}.dropdown-context-up:after,.dropdown-context:after{border-bottom:6px solid var(--lsky-surface);border-left:6px solid transparent;border-right:6px solid transparent;content:"";display:inline-block;left:10px;position:absolute;top:-6px}.dropdown-context-up:after,.dropdown-context-up:before{bottom:-7px;top:auto;z-index:9999}.dropdown-context-up:before{border-bottom:none;border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid var(--lsky-border-2)}.dropdown-context-up:after{border-bottom:none;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid var(--lsky-surface)}.dropdown-context-sub:after,.dropdown-context-sub:before{display:none}.compressed-context a{font-size:13.5px;padding-bottom:0;padding-left:14px;padding-top:0}.compressed-context .divider{margin:5px 1px}.compressed-context .nav-header{padding:1px 13px}
|
||||
.nav-header{color:var(--lsky-text-3);display:block;font-size:12.5px;font-weight:700;line-height:20px;padding:3px 15px;text-shadow:none;text-transform:uppercase}.dropdown-menu{background-clip:padding-box;background-color:var(--lsky-surface);border:0;border-radius:6px;box-shadow:var(--lsky-shadow-pop);display:none;float:left;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;left:0;list-style:none;margin:2px 0 0;min-width:160px;padding:5px 0;position:absolute;text-align:left;top:100%;z-index:1000}.dropdown-menu.pull-right{left:auto;right:0}.dropdown-menu .divider{background-color:var(--lsky-surface-3);border-bottom:1px solid var(--lsky-border);height:1px;margin:9px 1px;overflow:hidden}.dropdown-menu a{clear:both;color:var(--lsky-text);display:block;font-weight:400;line-height:20px;padding:5px 20px;text-decoration:none;white-space:nowrap}.dropdown-menu .disabled>a:hover{background-color:transparent;cursor:default;text-decoration:none}.dropdown-menu .active>a,.dropdown-menu .active>a:hover,.dropdown-menu .dropdown-submenu:hover>a,.dropdown-menu li>a:focus,.dropdown-menu li>a:hover{background-color:var(--lsky-accent);background-image:none;background-repeat:repeat-x;color:#fff;text-decoration:none}.dropdown-menu .active>a,.dropdown-menu .active>a:hover{outline:0}.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:var(--lsky-text-3)}.dropdown-menu .open{*z-index:1000}.dropdown-menu .open>.dropdown-menu{display:block}.dropdown-menu .pull-right>.dropdown-menu{left:auto;right:0}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-bottom:4px solid var(--lsky-border-2);border-top:0;content:"\2191"}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{bottom:100%;margin-bottom:1px;top:auto}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{border-radius:0 6px 6px 6px;left:100%;margin-left:-1px;margin-top:-6px;top:0}.dropdown-submenu>.dropdown-menu.drop-left{left:-100%}.dropdown-submenu:hover .dropdown-menu{display:block}.dropdown-submenu:hover>a:after{border-left-color:var(--lsky-surface)}.dropdown-submenu>a:after{border-bottom:5px solid transparent;border-left:5px solid transparent;border-left-color:var(--lsky-border-2);border-right:0 solid transparent;border-top:5px solid transparent;content:" ";display:block;float:right;height:0;margin-right:-10px;margin-top:5px;width:0}.dropdown .dropdown-menu .nav-header{padding-left:20px;padding-right:20px}.dropdown-context .nav-header{cursor:default}.dropdown-context .dropdown-submenu:hover .dropdown-menu{display:none}.dropdown-context .dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-context .submenu-back>a>i{margin-right:7px}.dropdown-context-up:before,.dropdown-context:before{border-bottom:7px solid rgba(0,0,0,.2);border-left:7px solid transparent;border-right:7px solid transparent;content:"";display:inline-block;left:9px;position:absolute;top:-7px}.dropdown-context-up:after,.dropdown-context:after{border-bottom:6px solid var(--lsky-surface);border-left:6px solid transparent;border-right:6px solid transparent;content:"";display:inline-block;left:10px;position:absolute;top:-6px}.dropdown-context-up:after,.dropdown-context-up:before{bottom:-7px;top:auto;z-index:9999}.dropdown-context-up:before{border-bottom:none;border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid var(--lsky-border-2)}.dropdown-context-up:after{border-bottom:none;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid var(--lsky-surface)}.dropdown-context-sub:after,.dropdown-context-sub:before{display:none}.compressed-context a{font-size:13.5px;padding-bottom:0;padding-left:14px;padding-top:0}.compressed-context .divider{margin:5px 1px}.compressed-context .nav-header{padding:1px 13px}
|
||||
@@ -167,6 +167,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端二级菜单「就地展开」时,顶部那行「< 返回」里的箭头与文字贴在一起很难看:
|
||||
箭头是图标(fas fa-chevron-left,标记里自带 mr-1 ≈ 4px),这里统一补到 7px 间距。
|
||||
三条类选择器的权重高于工具类 .mr-1,所以不依赖 CSS 打包顺序;其余样式一个字没动。 */
|
||||
.dropdown-context .submenu-back > a > i {
|
||||
margin-right: 7px;
|
||||
}
|
||||
.dropdown-context:before, .dropdown-context-up:before {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
|
||||
@@ -104,6 +104,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 图片「详细信息」与「移动到相册」改用居中卡片弹窗(复用 components/modal.blade.php,
|
||||
手机上它就是「底部抽屉式」——弹窗容器在 <640px 时贴底、不依赖 hover)。
|
||||
右侧抽屉 #drawer 保留它原来的本职:顶部工具栏的「相册列表」入口照旧走抽屉。 --}}
|
||||
<x-modal id="image-detail-modal">
|
||||
<div id="image-detail-content"></div>
|
||||
</x-modal>
|
||||
|
||||
<x-modal id="image-movements-modal">
|
||||
<div id="image-movements-content"></div>
|
||||
</x-modal>
|
||||
|
||||
<script type="text/html" id="images-item-tpl">
|
||||
<a href="javascript:void(0)" data-id="__id__" data-json='__json__' class="images-item relative cursor-default rounded outline outline-2 outline-offset-2 outline-transparent">
|
||||
<div class="image-selector absolute z-[2] top-0 right-0 overflow-hidden cursor-pointer sm:hidden group-hover:block">
|
||||
@@ -159,59 +170,93 @@
|
||||
</div>
|
||||
</script>
|
||||
|
||||
{{-- 图片「详细信息」:居中卡片弹窗(不再渲染进右侧抽屉)。
|
||||
字段顺序按老师要求:**上传时间第一、图片名称紧跟其后**,其余字段一个没删、只换了位置
|
||||
(相册名称 / 使用策略 / 图片原始名称 / 图片大小 / 图片类型 / 尺寸 / MD5 / SHA-128 / 权限 / 上传 IP)。
|
||||
排版:标签在左、值在右(窄屏 flex-col 自动上下堆叠);标签 13px、值 14px,行高 leading-6 宽松一些。
|
||||
顶部一张小缩略图。这里不放任何快捷操作(复制链接/下载/删除都不进这个弹窗)。 --}}
|
||||
<script type="text/html" id="image-detail-tpl">
|
||||
<div class="my-4 px-4 space-y-3">
|
||||
<div>
|
||||
<span class="text-sm font-semibold">相册名称</span>
|
||||
<p class="my-2 break-words text-ink">__album_name__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">使用策略</div>
|
||||
<p class="my-2 break-words text-ink-2">__strategy_name__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片名称</div>
|
||||
<p class="my-2 break-words text-ink-2">__filename__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片原始名称</div>
|
||||
<p class="my-2 break-words text-ink-2">__origin_name__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片大小</div>
|
||||
<p class="my-2 break-words text-ink-2">__size__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">图片类型</div>
|
||||
<p class="my-2 break-words text-ink-2">__mimetype__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">尺寸</div>
|
||||
<p class="my-2 break-words text-ink-2">__width__ * __height__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">MD5</div>
|
||||
<p class="my-2 break-words text-ink-2">__md5__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">SHA-128</div>
|
||||
<p class="my-2 break-words text-ink-2">__sha1__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">权限</div>
|
||||
<p class="my-2 break-words text-ink-2">__permission__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">上传 IP</div>
|
||||
<p class="my-2 break-words text-ink-2">__uploaded_ip__</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">上传时间</div>
|
||||
<p class="my-2 break-words text-ink-2">__created_at__</p>
|
||||
<div class="mx-auto w-full max-w-2xl">
|
||||
<img src="__thumb_url__" alt="__filename__" class="mb-4 h-20 w-20 rounded-lg border border-line bg-surface-2 object-cover">
|
||||
<dl class="space-y-3">
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">上传时间</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__created_at__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片名称</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__filename__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">相册名称</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__album_name__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">使用策略</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__strategy_name__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片原始名称</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__origin_name__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片大小</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__size__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">图片类型</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__mimetype__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">尺寸</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__width__ * __height__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">MD5</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__md5__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">SHA-128</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__sha1__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">权限</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__permission__</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 sm:flex-row sm:gap-4">
|
||||
<dt class="shrink-0 text-[13px] leading-6 text-ink-3 sm:w-28">上传 IP</dt>
|
||||
<dd class="min-w-0 break-words text-[14px] leading-6 text-ink">__uploaded_ip__</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
{{-- 「移动到相册」弹窗:相册列表(单选)+ 底部「移动」「取消」。
|
||||
列表数据与顶部工具栏的「相册列表」同一个接口;点一行只选中,底部「移动」才提交。
|
||||
每行 min-h-[44px](手机点击区 ≥44px),当前所在相册带「当前」标记(__current_badge__)。 --}}
|
||||
<script type="text/html" id="movements-container-tpl">
|
||||
<div class="mx-auto flex w-full max-w-xl flex-col">
|
||||
<p class="text-[15px] font-semibold text-ink">移动到相册</p>
|
||||
<p class="mt-0.5 mb-3 text-[13px] text-ink-3">已选择 __count__ 张图片</p>
|
||||
<div id="movements-albums" class="flex max-h-[50vh] w-full flex-col space-y-1 overflow-y-auto pr-1"></div>
|
||||
<div class="mt-4 flex justify-end gap-2 border-t border-line pt-3">
|
||||
<button type="button" id="movements-cancel" class="ls-btn h-11 px-4 sm:h-9">取消</button>
|
||||
<button type="button" id="movements-confirm" class="ls-btn ls-btn-primary h-11 px-4 sm:h-9" disabled>移动</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
{{-- 相册列表行:整行是一个点击区(min-h-[44px]),不用 span(避免被无限加载的
|
||||
"点 span 加载更多"委托命中)。选中态由 JS 切换 border-brand/bg-brand-soft/text-brand。 --}}
|
||||
<script type="text/html" id="movements-album-item-tpl">
|
||||
<a href="javascript:void(0)" data-id="__id__" data-selected="false" class="movements-album flex min-h-[44px] w-full items-center gap-2 rounded-lg border border-line bg-surface-2 px-3 py-2 text-ink hover:bg-surface-3">
|
||||
<i class="selected-mark fas fa-check-circle w-4 shrink-0 text-brand opacity-0" aria-hidden="true"></i>
|
||||
<div class="min-w-0 flex-1 truncate text-[14px]">__name__</div>
|
||||
__current_badge__
|
||||
<div class="shrink-0 text-[13px] text-ink-3">__image_num__</div>
|
||||
</a>
|
||||
</script>
|
||||
|
||||
@push('scripts')
|
||||
<script src="{{ asset('js/justified-gallery/jquery.justifiedGallery.min.js') }}"></script>
|
||||
<script src="{{ asset('js/viewer-js/viewer.min.js') }}"></script>
|
||||
@@ -241,6 +286,11 @@
|
||||
const $photos = $(IMAGES_GRID);
|
||||
const $drawer = $("#drawer");
|
||||
const $drawerMask = $('#drawer-mask');
|
||||
// 居中卡片弹窗(复用 components/modal.blade.php 的 Alpine store,用法同 admin 页)
|
||||
const modal = Alpine.store('modal');
|
||||
// 「移动到相册」弹窗里的相册列表容器 / 底部按钮
|
||||
const MOVEMENTS_MODAL = 'image-movements-modal';
|
||||
const DETAIL_MODAL = 'image-detail-modal';
|
||||
const viewer = new Viewer(document.getElementById('images-grid'), {url: 'data-original'});
|
||||
const drawer = {
|
||||
open(title, content, callback) {
|
||||
@@ -572,25 +622,94 @@ classes: ['dragselect'],
|
||||
|
||||
const methods = {
|
||||
movements() {
|
||||
getAlbums({title: '选择相册'}, e => {
|
||||
let selected = ds.getSelection().map(item => $(item).data('id'));
|
||||
$headerTitle.text(`移动 ${selected.length} 张图片至...`)
|
||||
$(e).off('click', '>a').on('click', '>a', function () {
|
||||
axios.put('{{ route('user.images.movement') }}', {
|
||||
selected: selected,
|
||||
id: $(this).data('id'),
|
||||
album_id: selectedAlbum.id || 0,
|
||||
}).then(response => {
|
||||
if (response.data.status) {
|
||||
drawer.close();
|
||||
resetImages();
|
||||
toastr.success(response.data.message);
|
||||
} else {
|
||||
toastr.warning(response.data.message);
|
||||
}
|
||||
})
|
||||
// 「移动到相册」改用居中卡片弹窗(不再渲染进右侧抽屉 ——
|
||||
// 抽屉只留给顶部工具栏的「相册列表」入口)。
|
||||
// 相册列表仍走同一个 user.albums 接口 + 同一个 utils.infiniteScroll;
|
||||
// 弹窗内是单选列表(当前所在相册带「当前」标记),底部「移动」「取消」。
|
||||
let selected = ds.getSelection().map(item => $(item).data('id'));
|
||||
if (! selected.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$('#image-movements-content').html(
|
||||
$('#movements-container-tpl').html().replace(/__count__/g, selected.length)
|
||||
);
|
||||
|
||||
const $movementsAlbums = $('#movements-albums');
|
||||
const $movementsConfirm = $('#movements-confirm');
|
||||
let targetId = null; // 本次要移动到的相册(单选)
|
||||
|
||||
// 单选:点一行只选中并高亮,不发请求;底部「移动」才提交
|
||||
const selectRow = ($row) => {
|
||||
targetId = $row.length ? $row.data('id') : null;
|
||||
$movementsAlbums.find('.movements-album').each(function () {
|
||||
let on = targetId !== null && $(this).data('id') === targetId;
|
||||
$(this)
|
||||
.toggleClass('border-brand bg-brand-soft text-brand', on)
|
||||
.toggleClass('border-line bg-surface-2 text-ink', ! on)
|
||||
.attr('data-selected', on ? 'true' : 'false')
|
||||
.find('.selected-mark').toggleClass('opacity-0', ! on);
|
||||
});
|
||||
$movementsConfirm.prop('disabled', targetId === null);
|
||||
};
|
||||
|
||||
utils.infiniteScroll('#movements-albums', {
|
||||
url: '{{ route('user.albums') }}',
|
||||
success: function (response) {
|
||||
if (! response.status) {
|
||||
return toastr.error(response.message);
|
||||
}
|
||||
|
||||
let albums = response.data.albums.data;
|
||||
if (albums.length <= 0 || response.data.albums.current_page === response.data.albums.last_page) {
|
||||
this.finished = true;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const i in albums) {
|
||||
html += $('#movements-album-item-tpl').html()
|
||||
.replace(/__id__/g, albums[i].id)
|
||||
.replace(/__name__/g, albums[i].name)
|
||||
.replace(/__image_num__/g, albums[i].image_num)
|
||||
// 当前所在相册标出来
|
||||
.replace(/__current_badge__/g, albums[i].id === selectedAlbum.id
|
||||
? '<div class="ls-badge shrink-0 bg-brand-soft text-brand">当前</div>'
|
||||
: '')
|
||||
}
|
||||
|
||||
$movementsAlbums.append(html);
|
||||
}
|
||||
});
|
||||
|
||||
$movementsAlbums.off('click', '.movements-album').on('click', '.movements-album', function () {
|
||||
selectRow($(this));
|
||||
});
|
||||
|
||||
$('#movements-cancel').off('click').on('click', _ => modal.close(MOVEMENTS_MODAL));
|
||||
|
||||
// 移动请求沿用原实现(同一个 route、同一份 payload),
|
||||
// 唯一区别:原来「点哪行就移动哪行」,现在是「选中后点移动」
|
||||
$movementsConfirm.off('click').on('click', function () {
|
||||
if (targetId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
axios.put('{{ route('user.images.movement') }}', {
|
||||
selected: selected,
|
||||
id: targetId,
|
||||
album_id: selectedAlbum.id || 0,
|
||||
}).then(response => {
|
||||
if (response.data.status) {
|
||||
modal.close(MOVEMENTS_MODAL);
|
||||
resetImages();
|
||||
toastr.success(response.data.message);
|
||||
} else {
|
||||
toastr.warning(response.data.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
modal.open(MOVEMENTS_MODAL);
|
||||
},
|
||||
remove() {
|
||||
let selected = ds.getSelection().map(item => $(item).data('id'));
|
||||
@@ -721,6 +840,7 @@ classes: ['dragselect'],
|
||||
if (response.data.status) {
|
||||
let image = response.data.data.image;
|
||||
let content = $('#image-detail-tpl').html()
|
||||
.replace(/__thumb_url__/g, image.thumb_url)
|
||||
.replace(/__album_name__/g, image.album ? image.album.name : '-')
|
||||
.replace(/__strategy_name__/g, image.strategy ? image.strategy.name : '-')
|
||||
.replace(/__filename__/g, image.filename.replace(/\$/g, '$$$$'))
|
||||
@@ -734,7 +854,9 @@ classes: ['dragselect'],
|
||||
.replace(/__permission__/g, image.permission === 1 ? '公开' : '私有')
|
||||
.replace(/__uploaded_ip__/g, image.uploaded_ip)
|
||||
.replace(/__created_at__/g, image.created_at)
|
||||
drawer.open(item.filename, content);
|
||||
// 「详细信息」改成居中卡片弹窗(原来画进右侧抽屉)
|
||||
$('#image-detail-content').html(content);
|
||||
modal.open(DETAIL_MODAL);
|
||||
} else {
|
||||
toastr.error(response.data.message);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* 「详细信息 / 移动到相册」新弹窗的行为测试(jsdom + 真实 jQuery)
|
||||
*
|
||||
* 与 images-modal.test.mjs(静态断言)互补:这里把 images.blade.php 里 @push('scripts')
|
||||
* 的三段内联脚本真的跑起来(周边依赖打桩),然后驱动新代码路径:
|
||||
* 1. methods.movements() → 打开居中卡片弹窗(modal.open),列表渲染进 #movements-albums
|
||||
* 2. 点一行 = 只选中(单选、高亮、「当前」标记、≥44px 的行),不发请求
|
||||
* 3. 点「移动」才发请求,且 payload 与原来完全一致(selected / id / album_id)
|
||||
* 4. 「取消」只关弹窗
|
||||
* 5. methods.detail() → 内容渲染进 #image-detail-content,字段顺序 上传时间 → 图片名称
|
||||
* 6. 全程右侧抽屉一点没被这两项用上(#drawer-content 保持空、抽屉没被推开)
|
||||
*
|
||||
* 运行:node images-modal-dom.test.mjs
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
const here = path.dirname(decodeURIComponent(new URL(import.meta.url).pathname));
|
||||
const SRC = path.join(here, '..', 'src');
|
||||
const blade = fs.readFileSync(path.join(SRC, 'resources', 'views', 'user', 'images.blade.php'), 'utf8');
|
||||
const JQUERY_SRC = fs.readFileSync(path.join(here, 'node_modules/jquery/dist/jquery.js'), 'utf8');
|
||||
|
||||
// 三段内联脚本(按出现顺序);blade 表达式换成桩串(route() 之类)
|
||||
function inlineScripts() {
|
||||
const tail = blade.slice(blade.indexOf("@push('scripts')"));
|
||||
const out = [];
|
||||
const re = /<script>([\s\S]*?)<\/script>/g;
|
||||
let m;
|
||||
while ((m = re.exec(tail))) {
|
||||
out.push(m[1].replace(/\{\{[\s\S]*?\}\}/g, '/stub').replace(/\{!![\s\S]*?!!\}/g, ''));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function tpl(id) {
|
||||
const start = blade.indexOf(`<script type="text/html" id="${id}">`);
|
||||
const end = blade.indexOf('</script>', start);
|
||||
return blade.slice(blade.indexOf('>', start) + 1, end);
|
||||
}
|
||||
|
||||
const IMAGE = {
|
||||
id: 7, filename: 'beach.jpg', origin_name: 'IMG_0001.HEIC', url: 'https://i.example.com/beach.jpg',
|
||||
thumb_url: 'https://i.example.com/beach-thumb.jpg', width: 4000, height: 3000, size: 2048,
|
||||
mimetype: 'image/jpeg', md5: 'abcdef', sha1: '123456', permission: 0, uploaded_ip: '1.2.3.4',
|
||||
created_at: '2026-09-01 10:00:00', album: { name: '旧相册' }, strategy: { name: '本机' }, links: {},
|
||||
};
|
||||
|
||||
function boot() {
|
||||
const dom = new JSDOM(`<!DOCTYPE html><html><body>
|
||||
<span id="header-title"></span>
|
||||
<div id="images-scroll"><div id="images-grid">
|
||||
<a class="images-item" data-id="7" href="javascript:void(0)"
|
||||
data-json='${JSON.stringify(IMAGE).replace(/'/g, ''')}'>
|
||||
<img alt="beach.jpg" data-original="https://i.example.com/beach.jpg" src="https://i.example.com/beach-thumb.jpg">
|
||||
</a>
|
||||
</div></div>
|
||||
<div id="drawer-mask" class="hidden"></div>
|
||||
<div id="drawer"><span id="drawer-title"></span><div id="drawer-content"></div></div>
|
||||
<div id="image-detail-modal" class="hidden"><div id="image-detail-content"></div></div>
|
||||
<div id="image-movements-modal" class="hidden"><div id="image-movements-content"></div></div>
|
||||
<a data-operate="movements" class="hidden" href="javascript:void(0)">移动到相册</a>
|
||||
<a data-operate="detail" class="hidden" href="javascript:void(0)">详细信息</a>
|
||||
<input id="search">
|
||||
<script type="text/html" id="image-detail-tpl">${tpl('image-detail-tpl')}</script>
|
||||
<script type="text/html" id="movements-container-tpl">${tpl('movements-container-tpl')}</script>
|
||||
<script type="text/html" id="movements-album-item-tpl">${tpl('movements-album-item-tpl')}</script>
|
||||
<script type="text/html" id="albums-container-tpl"><div id="albums-container"></div></script>
|
||||
<script type="text/html" id="albums-item-tpl"><a class="albums-item" data-id="__id__" data-json='__json__' title="__intro__"><span class="name">__name__</span></a></script>
|
||||
</body></html>`, { runScripts: 'outside-only', pretendToBeVisual: true, url: 'https://lsky.test/user/images' });
|
||||
|
||||
const { window } = dom;
|
||||
Object.defineProperty(window.navigator, 'userAgent', { value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', configurable: true });
|
||||
|
||||
window.eval(JQUERY_SRC);
|
||||
const $ = window.jQuery;
|
||||
$.fn.justifiedGallery = function () { return this; };
|
||||
|
||||
const calls = { modalOpen: [], modalClose: [], puts: [], gets: [], toasts: [], infiniteScroll: [], attaches: [], refreshes: [] };
|
||||
|
||||
const stores = {
|
||||
modal: {
|
||||
open: (id) => calls.modalOpen.push(id),
|
||||
close: (id) => calls.modalClose.push(id),
|
||||
isOpen: () => false,
|
||||
},
|
||||
};
|
||||
window.Alpine = { store: (n) => stores[n] };
|
||||
|
||||
window.axios = {
|
||||
put: (url, data) => { calls.puts.push({ url, data }); return Promise.resolve({ data: { status: true, message: '移动成功' } }); },
|
||||
get: (url) => { calls.gets.push(url); return Promise.resolve({ data: { status: true, data: { image: IMAGE } } }); },
|
||||
delete: () => Promise.resolve({ data: { status: true, message: 'ok' } }),
|
||||
};
|
||||
|
||||
window.DragSelect = class {
|
||||
constructor() { window.DragSelect.last = this; this._sel = []; }
|
||||
getSelection() { return this._sel; }
|
||||
subscribe() {} setSelectables() {} addSelection() {} toggleSelection() {} clearSelection() {}
|
||||
stop() {} break() {}
|
||||
};
|
||||
window.Viewer = class { update() {} };
|
||||
window.ClipboardJS = class { on() { return this; } };
|
||||
window.context = { init() {}, attach: (s) => calls.attaches.push(s), isMenuOpen: () => false };
|
||||
window.toastr = {
|
||||
success: (m) => calls.toasts.push(['success', m]),
|
||||
warning: (m) => calls.toasts.push(['warning', m]),
|
||||
error: (m) => calls.toasts.push(['error', m]),
|
||||
};
|
||||
window.Swal = { fire: () => Promise.resolve({ isConfirmed: false }) };
|
||||
window.utils = {
|
||||
formatSize: (bytes) => `${bytes} B`,
|
||||
isMobile: () => false,
|
||||
setCapacityProgress() {},
|
||||
infiniteScroll(selector, options) {
|
||||
calls.infiniteScroll.push({ selector, options });
|
||||
return { refresh(params) { calls.refreshes.push(params); }, reset() {}, destroy() {} };
|
||||
},
|
||||
};
|
||||
|
||||
// jsdom 的 window.eval 每次都是独立的词法作用域(浏览器里 classic script 之间共享全局词法
|
||||
// 环境的行为在 vm 里没有),所以三段脚本拼成一个源一起跑,并在同一作用域里把内部引用抛出来。
|
||||
window.eval(inlineScripts().join('\n') + `
|
||||
window.__t = {
|
||||
methods,
|
||||
getAlbums,
|
||||
drawer,
|
||||
ds,
|
||||
setSelectedAlbum(v) { selectedAlbum = v; },
|
||||
getSelectedAlbum() { return selectedAlbum; },
|
||||
};
|
||||
`);
|
||||
|
||||
return { window, $, calls, t: window.__t };
|
||||
}
|
||||
|
||||
const results = [];
|
||||
function check(name, pass, detail = '') {
|
||||
results.push({ name, pass, detail });
|
||||
console.log(`${pass ? ' PASS' : ' FAIL'} ${name}${detail ? ' → ' + detail : ''}`);
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// ================================================================ 移动到相册
|
||||
console.log('\n[移动到相册] 弹窗路径走通,抽屉一点没碰');
|
||||
{
|
||||
const { $, calls, t } = boot();
|
||||
t.ds._sel = [$('.images-item').get(0)];
|
||||
// 当前所在相册(用来验证「当前」标记 + album_id 原值)
|
||||
t.setSelectedAlbum({ id: 9, name: '旧相册' });
|
||||
|
||||
t.methods.movements();
|
||||
|
||||
check('打开了新弹窗(modal.open("image-movements-modal"))',
|
||||
calls.modalOpen.length === 1 && calls.modalOpen[0] === 'image-movements-modal', JSON.stringify(calls.modalOpen));
|
||||
check('内容渲染进 #image-movements-content,标题带已选数量',
|
||||
$('#image-movements-content').text().includes('移动到相册') && $('#image-movements-content').text().includes('已选择 1 张图片'));
|
||||
check('相册列表挂载在 #movements-albums(不是 #drawer-content)',
|
||||
$('#movements-albums').length === 1 && $('#drawer-content').html() === '',
|
||||
`drawer-content=${JSON.stringify($('#drawer-content').html())}`);
|
||||
check('抽屉没有被推开(#drawer 的 right 没被动过)',
|
||||
$('#drawer').css('right') !== '0px' && $('#drawer').css('right') !== '0', String($('#drawer').css('right')));
|
||||
check('底部「移动」「取消」在,且「移动」默认 disabled',
|
||||
$('#movements-confirm').length === 1 && $('#movements-confirm').prop('disabled') === true && $('#movements-cancel').length === 1);
|
||||
|
||||
const scroll = calls.infiniteScroll.at(-1);
|
||||
check('相册列表走 utils.infiniteScroll,容器是 #movements-albums',
|
||||
scroll?.selector === '#movements-albums', String(scroll?.selector));
|
||||
|
||||
// 喂一页相册数据(模拟 user.albums 的响应)
|
||||
scroll.options.success.call({ finished: false }, {
|
||||
status: true,
|
||||
data: { albums: { data: [{ id: 1, name: '三亚', image_num: 2 }, { id: 9, name: '旧相册', image_num: 0 }], current_page: 1, last_page: 1 } },
|
||||
});
|
||||
|
||||
const rows = $('#movements-albums .movements-album');
|
||||
check('渲染出两行相册,data-id 正确', rows.length === 2 && rows.eq(0).data('id') === 1 && rows.eq(1).data('id') === 9,
|
||||
`${rows.length} 行`);
|
||||
check('每行整行可点(min-h-[44px] 的手机点击区)', rows.eq(0).attr('class').includes('min-h-[44px]'));
|
||||
check('当前所在相册(id=9)带「当前」标记,别的行没有',
|
||||
rows.eq(1).find('.ls-badge').text() === '当前' && rows.eq(0).find('.ls-badge').length === 0,
|
||||
rows.eq(1).text().trim());
|
||||
check('行里没有 span(避免命中「点 span 加载更多」的委托)', rows.eq(0).find('span').length === 0);
|
||||
|
||||
rows.eq(0).trigger('click');
|
||||
check('点一行:选中(data-selected=true + 品牌色高亮 + 对勾出现)',
|
||||
rows.eq(0).attr('data-selected') === 'true'
|
||||
&& rows.eq(0).attr('class').includes('bg-brand-soft')
|
||||
&& ! rows.eq(0).find('.selected-mark').attr('class').includes('opacity-0'));
|
||||
check('点一行:没有立刻发请求(等底部「移动」)', calls.puts.length === 0);
|
||||
check('点一行:「移动」变为可用', $('#movements-confirm').prop('disabled') === false);
|
||||
|
||||
rows.eq(1).trigger('click');
|
||||
check('单选:点第二行后第一行回到未选中',
|
||||
rows.eq(0).attr('data-selected') === 'false' && rows.eq(1).attr('data-selected') === 'true'
|
||||
&& ! rows.eq(0).attr('class').includes('bg-brand-soft'));
|
||||
check('单选:对勾也跟着切换',
|
||||
rows.eq(0).find('.selected-mark').attr('class').includes('opacity-0')
|
||||
&& ! rows.eq(1).find('.selected-mark').attr('class').includes('opacity-0'));
|
||||
|
||||
$('#movements-confirm').trigger('click');
|
||||
check('点「移动」:请求沿用原实现(同一 route + selected/id/album_id)',
|
||||
calls.puts.length === 1 && calls.puts[0].url === '/stub'
|
||||
&& JSON.stringify(calls.puts[0].data) === JSON.stringify({ selected: [7], id: 9, album_id: 9 }),
|
||||
JSON.stringify(calls.puts[0]));
|
||||
|
||||
await sleep(20);
|
||||
check('移动成功后:关弹窗 + toastr.success',
|
||||
calls.modalClose.includes('image-movements-modal') && calls.toasts.some((t) => t[0] === 'success'),
|
||||
JSON.stringify(calls.toasts));
|
||||
}
|
||||
|
||||
// ================================================================ 取消
|
||||
console.log('\n[移动到相册] 「取消」只关弹窗');
|
||||
{
|
||||
const { $, calls, t } = boot();
|
||||
t.ds._sel = [$('.images-item').get(0)];
|
||||
t.methods.movements();
|
||||
$('#movements-cancel').trigger('click');
|
||||
check('点「取消」:关弹窗且没有发任何请求',
|
||||
calls.modalClose.includes('image-movements-modal') && calls.puts.length === 0,
|
||||
JSON.stringify(calls.modalClose));
|
||||
}
|
||||
|
||||
// ================================================================ 详细信息
|
||||
console.log('\n[详细信息] 弹窗路径 + 字段顺序(上传时间第一、图片名称第二)');
|
||||
{
|
||||
const { $, calls, t } = boot();
|
||||
t.methods.detail($('.images-item').get(0));
|
||||
await sleep(20);
|
||||
|
||||
check('请求走原接口(/user/images/:id)', calls.gets.length === 1 && calls.gets[0] === '/user/images/7', JSON.stringify(calls.gets));
|
||||
check('渲染进 #image-detail-content 并打开 #image-detail-modal',
|
||||
$('#image-detail-content').text().includes('beach.jpg')
|
||||
&& calls.modalOpen.includes('image-detail-modal'), JSON.stringify(calls.modalOpen));
|
||||
check('内容没有进右侧抽屉(#drawer-content 仍是空的)', $('#drawer-content').html() === '');
|
||||
|
||||
const labels = $('#image-detail-content dt').map(function () { return $(this).text().trim(); }).get();
|
||||
check('字段一个不少(12 项,与改前一致)', labels.length === 12, labels.join(' / '));
|
||||
check('顺序:上传时间 第一、图片名称 第二', labels[0] === '上传时间' && labels[1] === '图片名称', labels.join(' / '));
|
||||
check('其余字段保持原有相对顺序',
|
||||
JSON.stringify(labels.slice(2)) === JSON.stringify(['相册名称', '使用策略', '图片原始名称', '图片大小', '图片类型', '尺寸', 'MD5', 'SHA-128', '权限', '上传 IP']),
|
||||
labels.slice(2).join(' / '));
|
||||
check('小缩略图用的是 thumb_url', $('#image-detail-content img').attr('src') === IMAGE.thumb_url);
|
||||
check('值是渲染后的真实内容(不是占位符)',
|
||||
$('#image-detail-content').text().includes('2026-09-01 10:00:00') && $('#image-detail-content').text().includes('2097152 B')
|
||||
&& ! $('#image-detail-content').text().includes('__'));
|
||||
check('标签 13px、值 14px(text-[13px] / text-[14px])',
|
||||
$('#image-detail-content dt').first().attr('class').includes('text-[13px]')
|
||||
&& $('#image-detail-content dd').first().attr('class').includes('text-[14px]'));
|
||||
check('弹窗里没有复制链接/下载/删除这类快捷操作',
|
||||
!/复制|下载|删除/.test($('#image-detail-content').text()));
|
||||
}
|
||||
|
||||
|
||||
// ================================================================ 抽屉本职
|
||||
console.log('\n[抽屉] 顶部工具栏的「相册列表」入口照旧走右侧抽屉');
|
||||
{
|
||||
const { $, calls, t } = boot();
|
||||
t.getAlbums();
|
||||
|
||||
check('getAlbums() 打开的是抽屉(right 推到 0)而不是弹窗',
|
||||
String($('#drawer').css('right')) === '0px' && calls.modalOpen.length === 0,
|
||||
`right=${$('#drawer').css('right')} modalOpen=${JSON.stringify(calls.modalOpen)}`);
|
||||
check('相册列表内容渲染进 #drawer-content(#albums-container 在抽屉里)',
|
||||
$('#drawer-content #albums-container').length === 1 && $('#image-movements-content').html() === '');
|
||||
check('抽屉标题带「我的相册」',
|
||||
$('#drawer-title').text().includes('我的相册'), $('#drawer-title').text());
|
||||
check('抽屉里的列表仍挂在 #drawer-content 上做无限加载',
|
||||
calls.infiniteScroll.at(-1)?.selector === '#drawer-content', String(calls.infiniteScroll.at(-1)?.selector));
|
||||
|
||||
calls.infiniteScroll.at(-1).options.success.call({ finished: false }, {
|
||||
status: true,
|
||||
data: { albums: { data: [{ id: 3, name: '旅行', image_num: 5, intro: '' }], current_page: 1, last_page: 1 } },
|
||||
});
|
||||
check('抽屉里渲染出相册行(.albums-item,旧标记没变)',
|
||||
$('#drawer-content .albums-item').length === 1 && $('#drawer-content .albums-item').data('id') === 3);
|
||||
|
||||
// 点相册 → 筛选图片(resetImages),行为与改前一致
|
||||
calls.refreshes.length = 0;
|
||||
$('#drawer-content .albums-item').trigger('click');
|
||||
check('点抽屉里的相册:按该相册筛选图片(imagesInfinite.refresh 带上 album_id)',
|
||||
calls.refreshes.length === 1 && calls.refreshes[0].album_id === 3, JSON.stringify(calls.refreshes));
|
||||
check('点抽屉里的相册:抽屉关闭(right 回到 -1000px)',
|
||||
String($('#drawer').css('right')) === '-1000px', String($('#drawer').css('right')));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 汇总
|
||||
const failed = results.filter((r) => !r.pass);
|
||||
console.log(`\n${results.length - failed.length}/${results.length} 通过`);
|
||||
if (failed.length) {
|
||||
console.log('\n失败项:');
|
||||
failed.forEach((f) => console.log(` - ${f.name}${f.detail ? ' → ' + f.detail : ''}`));
|
||||
}
|
||||
process.exit(failed.length ? 1 : 0);
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 图片页「详细信息 / 移动到相册」改成居中卡片弹窗的回归测试(纯静态断言,不需要浏览器)
|
||||
*
|
||||
* 背景:这两项原来是画进右侧抽屉 #drawer 的;老师要求换成复用 x-modal 的居中卡片弹窗,
|
||||
* 抽屉只保留「顶部工具栏 → 相册列表」这一本职。这个文件就是把那次改动的契约钉住:
|
||||
* 1. 新弹窗的钩子都在,且两项确实不再走抽屉(drawer.open 那条调用没了)
|
||||
* 2. 抽屉那一套钩子一个都没被改名/删除(菜单绑定、data-operate、data-id、相册列表模板…)
|
||||
* 3. 「详细信息」字段顺序:上传时间第一、图片名称第二,其余字段一个不少
|
||||
* 4. 弹窗内容只用设计令牌(不出现硬编码颜色)
|
||||
* 5. 「移动」的请求沿用原实现(同一个 route、同一份 payload)
|
||||
* 6. 二级菜单「返回」的箭头与文字之间留了 6~8px 间距(改的是 .less,不是钉住的 context-js.js)
|
||||
* 7. 钉住的 context-js.js(两份拷贝)没被碰过 —— 直接对 Dockerfile 里钉的 md5
|
||||
*
|
||||
* 运行:node images-modal.test.mjs
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const here = path.dirname(decodeURIComponent(new URL(import.meta.url).pathname));
|
||||
const SRC = path.join(here, '..', 'src');
|
||||
const read = (...p) => fs.readFileSync(path.join(SRC, ...p), 'utf8');
|
||||
|
||||
const blade = read('resources', 'views', 'user', 'images.blade.php');
|
||||
const less = read('resources', 'css', 'context-js.less');
|
||||
const contextJs = read('resources', 'js', 'context-js.js');
|
||||
const contextJsPub = read('public', 'js', 'context-js', 'context-js.js');
|
||||
const dockerfile = fs.readFileSync(path.join(here, '..', 'Dockerfile'), 'utf8');
|
||||
|
||||
const results = [];
|
||||
function check(name, pass, detail = '') {
|
||||
results.push({ name, pass, detail });
|
||||
console.log(`${pass ? ' PASS' : ' FAIL'} ${name}${detail ? ' → ' + detail : ''}`);
|
||||
}
|
||||
|
||||
// 取出一整块 <script type="text/html" id="xxx"> … </script>
|
||||
function tpl(id) {
|
||||
const start = blade.indexOf(`<script type="text/html" id="${id}">`);
|
||||
if (start < 0) return '';
|
||||
const end = blade.indexOf('</script>', start);
|
||||
return end < 0 ? '' : blade.slice(start, end + '</script>'.length);
|
||||
}
|
||||
|
||||
const detailTpl = tpl('image-detail-tpl');
|
||||
const movementsContainerTpl = tpl('movements-container-tpl');
|
||||
const movementsItemTpl = tpl('movements-album-item-tpl');
|
||||
|
||||
// ---------------------------------------------------------------- 新弹窗钩子
|
||||
console.log('\n[新弹窗] 两个居中卡片弹窗(复用 x-modal)的钩子');
|
||||
{
|
||||
check('详细信息弹窗:<x-modal id="image-detail-modal"> + #image-detail-content',
|
||||
blade.includes('<x-modal id="image-detail-modal">') && blade.includes('id="image-detail-content"'));
|
||||
check('移动到相册弹窗:<x-modal id="image-movements-modal"> + #image-movements-content',
|
||||
blade.includes('<x-modal id="image-movements-modal">') && blade.includes('id="image-movements-content"'));
|
||||
check('弹窗按钮/容器:movements-albums / movements-confirm / movements-cancel',
|
||||
blade.includes('id="movements-albums"') && blade.includes('id="movements-confirm"') && blade.includes('id="movements-cancel"'));
|
||||
check('JS 里通过 Alpine 的 modal store 打开(modal.open(...))',
|
||||
/const modal = Alpine\.store\('modal'\);/.test(blade) && blade.includes('modal.open(MOVEMENTS_MODAL)') && blade.includes('modal.open(DETAIL_MODAL)'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 不再走抽屉
|
||||
console.log('\n[换实现] 两项都不再渲染进右侧抽屉,但「移动」的请求实现没被重写');
|
||||
{
|
||||
check('详细信息不再调 drawer.open(原调用已消失)',
|
||||
!blade.includes('drawer.open(item.filename, content)') && blade.includes("$('#image-detail-content').html(content);"));
|
||||
check('移动到相册不再借用抽屉的相册列表(原 getAlbums({title: \'选择相册\'}) 已消失)',
|
||||
!blade.includes("getAlbums({title: '选择相册'}"));
|
||||
check('移动请求沿用原实现:同一个 route + selected/id/album_id 三个字段',
|
||||
/axios\.put\('\{\{ route\('user\.images\.movement'\) \}\}', \{\s*selected: selected,\s*id: targetId,\s*album_id: selectedAlbum\.id \|\| 0,/.test(blade));
|
||||
check('第一版就带「取消」按钮,取消只关弹窗',
|
||||
/#movements-cancel'\)\.off\('click'\)\.on\('click', _ => modal\.close\(MOVEMENTS_MODAL\)\)/.test(blade));
|
||||
check('移动成功后关弹窗 + resetImages + toastr(沿用原来的收尾)',
|
||||
/modal\.close\(MOVEMENTS_MODAL\);\s*resetImages\(\);\s*toastr\.success\(response\.data\.message\);/.test(blade));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 抽屉本职保留
|
||||
console.log('\n[回归] 抽屉保留本职(顶部工具栏「相册列表」),且所有旧钩子都没被改名/删除');
|
||||
{
|
||||
const drawerHooks = [
|
||||
'id="drawer-mask"', 'id="drawer"', 'id="drawer-title"', 'id="drawer-content"',
|
||||
"const $drawer = $(\"#drawer\");", "const $drawerMask = $('#drawer-mask');",
|
||||
'const drawer = {', 'open(title, content, callback)', 'close(callback)', 'toggle(title, content, callback)',
|
||||
"drawer.find('#drawer-title').html(title)", "drawer.find('#drawer-content').html(content)",
|
||||
"utils.infiniteScroll('#drawer-content'", 'albumsInfinite', 'const getAlbums = (options, callback) =>',
|
||||
'drawer.toggle(title, content, function ()', 'drawer.close();',
|
||||
];
|
||||
const missing = drawerHooks.filter((h) => !blade.includes(h));
|
||||
check('抽屉钩子一个不少(id / drawer 对象 / 相册列表模板与无限加载)', missing.length === 0, missing.join(' | '));
|
||||
|
||||
const menuHooks = [
|
||||
'href="javascript:getAlbums()"',
|
||||
'data-operate="movements"', 'data-operate="detail"', 'data-operate="remove"',
|
||||
'data-operate="delete"', "case 'movements':", "case 'detail':",
|
||||
'context.attach(IMAGES_ITEM', 'context.attach(IMAGES_SCROLL',
|
||||
"id=\"images-grid\"", "id=\"images-scroll\"", 'class="images-item',
|
||||
"id=\"albums-container-tpl\"", "id=\"albums-item-tpl\"", "id=\"image-detail-tpl\"",
|
||||
'methods.movements()', 'methods.detail(selected[0])',
|
||||
'new ClipboardJS(\'.dropdown-menu li a.copy\'',
|
||||
];
|
||||
const missingMenu = menuHooks.filter((h) => !blade.includes(h));
|
||||
check('菜单/工具栏绑定一个不少(data-operate、右键菜单 actions、相册列表入口)', missingMenu.length === 0, missingMenu.join(' | '));
|
||||
|
||||
check('工具栏「相册列表」入口仍然走抽屉(getAlbums 内部仍 drawer.toggle)',
|
||||
blade.includes('drawer.toggle(title, content, function ()') && !blade.includes('modal.open(\'albums'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 详细信息字段顺序
|
||||
console.log('\n[详细信息] 字段顺序与一个都不能少');
|
||||
{
|
||||
const labels = ['上传时间', '图片名称', '相册名称', '使用策略', '图片原始名称', '图片大小', '图片类型',
|
||||
'尺寸', 'MD5', 'SHA-128', '权限', '上传 IP'];
|
||||
const missing = labels.filter((l) => !detailTpl.includes(`>${l}</dt>`));
|
||||
check('12 个字段全在(与改前一致,一个没删)', missing.length === 0, missing.join(' | '));
|
||||
|
||||
const at = (l) => detailTpl.indexOf(`>${l}</dt>`);
|
||||
check('顺序:上传时间 排第一', at('上传时间') > -1 && at('上传时间') === Math.min(...labels.map(at)));
|
||||
check('顺序:图片名称 紧跟在 上传时间 之后(第二个字段)',
|
||||
at('图片名称') === labels.map(at).sort((a, b) => a - b)[1]);
|
||||
check('其余字段保持原有相对顺序(相册名称 → 使用策略 → … → 上传 IP)',
|
||||
['相册名称', '使用策略', '图片原始名称', '图片大小', '图片类型', '尺寸', 'MD5', 'SHA-128', '权限', '上传 IP']
|
||||
.every((l, i, arr) => i === 0 || at(arr[i - 1]) < at(l)));
|
||||
|
||||
check('顶部有小缩略图(__thumb_url__,JS 里也做了替换)',
|
||||
detailTpl.includes('src="__thumb_url__"') && blade.includes('.replace(/__thumb_url__/g, image.thumb_url)'));
|
||||
check('标签 13px / 值 14px,行高宽松(leading-6)',
|
||||
(detailTpl.match(/text-\[13px\] leading-6 text-ink-3/g) || []).length === 12
|
||||
&& (detailTpl.match(/text-\[14px\] leading-6 text-ink/g) || []).length === 12);
|
||||
check('标签在左、值在右,窄屏可堆叠(sm:flex-row + flex-col)',
|
||||
detailTpl.includes('flex flex-col gap-0.5 sm:flex-row sm:gap-4'));
|
||||
check('弹窗里没有「复制链接/下载/删除」这类快捷操作',
|
||||
!/copy|Clipboard|download|delete|删除|复制链接/.test(detailTpl));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 移动弹窗
|
||||
console.log('\n[移动到相册] 单选 + 当前相册标记 + 手机点击区 ≥44px');
|
||||
{
|
||||
check('列表是单选:点击只选中(selectRow),不立刻发请求',
|
||||
blade.includes('const selectRow = ($row) =>') && !/on\('click', '\.movements-album', function \(\) \{\s*axios/.test(blade));
|
||||
check('当前所在相册标出来(__current_badge__ + selectedAlbum.id 比较)',
|
||||
movementsItemTpl.includes('__current_badge__') && blade.includes('albums[i].id === selectedAlbum.id'));
|
||||
check('每行点击区 ≥44px(min-h-[44px])', movementsItemTpl.includes('min-h-[44px]'));
|
||||
check('底部「移动」「取消」都在,移动默认禁用(没选相册不许提交)',
|
||||
/id="movements-confirm"[^>]*disabled/.test(movementsContainerTpl) && movementsContainerTpl.includes('id="movements-cancel"'));
|
||||
check('列表数据源与抽屉里的相册列表同一个接口(route user.albums)',
|
||||
blade.includes("url: '{{ route('user.albums') }}'"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 只用设计令牌
|
||||
console.log('\n[样式] 弹窗内容只用设计令牌,不硬编码颜色');
|
||||
{
|
||||
const blocks = { 'image-detail-tpl': detailTpl, 'movements-container-tpl': movementsContainerTpl, 'movements-album-item-tpl': movementsItemTpl };
|
||||
const bad = [];
|
||||
for (const [name, text] of Object.entries(blocks)) {
|
||||
if (/#[0-9a-fA-F]{3,8}\b/.test(text)) bad.push(`${name}: hex`);
|
||||
if (/\b(?:rgb|rgba|hsl)\(/.test(text)) bad.push(`${name}: rgb/hsl`);
|
||||
const hardcoded = text.match(/\b(?:text|bg|border)-(?:white|black|gray-\d+|red-\d+|green-\d+|blue-\d+|slate-\d+|zinc-\d+)\b/g);
|
||||
if (hardcoded) bad.push(`${name}: ${[...new Set(hardcoded)].join(',')}`);
|
||||
}
|
||||
check('三个模板里没有硬编码颜色', bad.length === 0, bad.join(' | '));
|
||||
|
||||
const tokens = ['bg-surface', 'bg-surface-2', 'bg-surface-3', 'text-ink', 'text-ink-3', 'border-line', 'bg-brand-soft', 'text-brand'];
|
||||
check('用到的都是设计令牌(surface / ink / line / brand)',
|
||||
tokens.filter((t) => (detailTpl + movementsContainerTpl + movementsItemTpl).includes(t)).length >= 6);
|
||||
check('选中态用令牌切换(border-brand / bg-brand-soft / text-brand)',
|
||||
blade.includes(".toggleClass('border-brand bg-brand-soft text-brand', on)"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 二级菜单「返回」
|
||||
console.log('\n[二级菜单] 「< 返回」的箭头与文字之间留了 6~8px');
|
||||
{
|
||||
const m = less.match(/\.dropdown-context \.submenu-back > a > i\s*\{\s*margin-right:\s*(\d+)px;\s*\}/);
|
||||
const px = m ? Number(m[1]) : NaN;
|
||||
check('.less 里给了 .dropdown-context .submenu-back > a > i 一个 6~8px 的 margin-right',
|
||||
!!m && px >= 6 && px <= 8, m ? `${px}px` : '没找到规则');
|
||||
check('改的是样式(.less),没有动钉住的 context-js.js 里的「返回」标记',
|
||||
contextJs.includes('<i class="fas fa-chevron-left mr-1"></i>返回'));
|
||||
// 选择器权重(0,3,2)压过工具类 .mr-1(0,1,0),所以不依赖打包顺序
|
||||
check('选择器权重高于工具类 .mr-1(3 个类 + 2 个元素 > 1 个类)', true, '.dropdown-context .submenu-back > a > i');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 钉住的补丁产物没被碰
|
||||
console.log('\n[钉住的产物] context-js.js(两份拷贝)与 Dockerfile 里钉的 md5 完全一致');
|
||||
{
|
||||
const pinned = (dockerfile.match(/^\s*'?([0-9a-f]{32})\s+\.\/(?:public|resources)\/js\/context-js(?:\/context-js)?\.js'?\s*\\?$/gm) || [])
|
||||
.map((l) => l.trim())
|
||||
.map((l) => l.replace(/^'?/, '').split(/\s+/)[0]);
|
||||
const md5 = (s) => crypto.createHash('md5').update(s).digest('hex');
|
||||
check('Dockerfile 里钉着两份 context-js.js 的 md5', pinned.length === 2, pinned.join(' / '));
|
||||
check('resources/js/context-js.js 未改动', pinned.includes(md5(contextJs)), md5(contextJs));
|
||||
check('public/js/context-js/context-js.js 未改动', pinned.includes(md5(contextJsPub)), md5(contextJsPub));
|
||||
check('那三个「资源版本串」标记还在(isIOSWebKit / LONG_PRESS_DELAY = 250 / submenu-inplace)',
|
||||
contextJs.includes('isIOSWebKit') && contextJs.includes('LONG_PRESS_DELAY = 250') && contextJs.includes('submenu-inplace'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 汇总
|
||||
const failed = results.filter((r) => !r.pass);
|
||||
console.log(`\n${results.length - failed.length}/${results.length} 通过`);
|
||||
if (failed.length) {
|
||||
console.log('\n失败项:');
|
||||
failed.forEach((f) => console.log(` - ${f.name}${f.detail ? ' → ' + f.detail : ''}`));
|
||||
}
|
||||
process.exit(failed.length ? 1 : 0);
|
||||
Reference in new issue
Block a user