fix:相机优化
This commit is contained in:
@@ -71,7 +71,6 @@ export default function ImageUpload({
|
|||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const cameraInputRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
const dragCounterRef = useRef(0)
|
const dragCounterRef = useRef(0)
|
||||||
const targetIdRef = useRef(0)
|
const targetIdRef = useRef(0)
|
||||||
const valueRef = useRef(value)
|
const valueRef = useRef(value)
|
||||||
@@ -258,21 +257,25 @@ export default function ImageUpload({
|
|||||||
</Upload>
|
</Upload>
|
||||||
{cameraEnabled && value.length < maxCount ? (
|
{cameraEnabled && value.length < maxCount ? (
|
||||||
<div className="image-upload-camera">
|
<div className="image-upload-camera">
|
||||||
|
{/*
|
||||||
|
相机入口用「透明原生 input 铺满按钮」的方式:
|
||||||
|
用户手指直接点在 input 上,浏览器才会把它当作拍照手势;
|
||||||
|
旧实现把 input 移到 left:-9999px 再由按钮脚本 .click(),部分安卓 WebView
|
||||||
|
会把相机 intent 降级成文件选择(表现为点了「拍照」却弹出文件浏览)。
|
||||||
|
*/}
|
||||||
|
<span className="camera-pick-trigger">
|
||||||
<input
|
<input
|
||||||
ref={cameraInputRef}
|
|
||||||
type="file"
|
type="file"
|
||||||
accept={cameraInputProps.accept}
|
accept={cameraInputProps.accept}
|
||||||
capture={cameraInputProps.capture}
|
capture={cameraInputProps.capture}
|
||||||
className="image-upload-camera-input"
|
className="image-upload-camera-input"
|
||||||
|
aria-label="拍照"
|
||||||
onChange={(event) => void handleCameraSelect(event)}
|
onChange={(event) => void handleCameraSelect(event)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button size="small" icon={<CameraOutlined />} tabIndex={-1}>
|
||||||
size="small"
|
|
||||||
icon={<CameraOutlined />}
|
|
||||||
onClick={() => cameraInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
拍照
|
拍照
|
||||||
</Button>
|
</Button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{showPasteHint ? (
|
{showPasteHint ? (
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export default function VideoUpload({
|
|||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const cameraInputRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
const [phase, setPhase] = useState<'idle' | 'compressing' | 'uploading'>('idle')
|
const [phase, setPhase] = useState<'idle' | 'compressing' | 'uploading'>('idle')
|
||||||
const [progress, setProgress] = useState(0)
|
const [progress, setProgress] = useState(0)
|
||||||
|
|
||||||
@@ -144,16 +143,6 @@ export default function VideoUpload({
|
|||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
/>
|
/>
|
||||||
{cameraEnabled ? (
|
|
||||||
<input
|
|
||||||
ref={cameraInputRef}
|
|
||||||
type="file"
|
|
||||||
accept={cameraInputProps.accept}
|
|
||||||
capture={cameraInputProps.capture}
|
|
||||||
className="worker-acceptance-video-camera-input"
|
|
||||||
onChange={handleSelect}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{value ? (
|
{value ? (
|
||||||
<div className="worker-acceptance-video-preview">
|
<div className="worker-acceptance-video-preview">
|
||||||
@@ -170,14 +159,22 @@ export default function VideoUpload({
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Space size={8} wrap>
|
<Space size={8} wrap>
|
||||||
{cameraEnabled ? (
|
{cameraEnabled ? (
|
||||||
<Button
|
// 透明原生 input 铺满按钮:真实手势点 input 才会被当作「拍摄」,
|
||||||
size="small"
|
// 脚本 click 在部分安卓 WebView 会退化成文件选择。
|
||||||
icon={<CameraOutlined />}
|
<span className="camera-pick-trigger">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept={cameraInputProps.accept}
|
||||||
|
capture={cameraInputProps.capture}
|
||||||
|
className="worker-acceptance-video-camera-input"
|
||||||
|
aria-label="重拍验收视频"
|
||||||
disabled={!canPick}
|
disabled={!canPick}
|
||||||
onClick={() => cameraInputRef.current?.click()}
|
onChange={handleSelect}
|
||||||
>
|
/>
|
||||||
|
<Button size="small" icon={<CameraOutlined />} disabled={!canPick} tabIndex={-1}>
|
||||||
重拍
|
重拍
|
||||||
</Button>
|
</Button>
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<Button size="small" disabled={!canPick} onClick={() => inputRef.current?.click()}>
|
<Button size="small" disabled={!canPick} onClick={() => inputRef.current?.click()}>
|
||||||
替换
|
替换
|
||||||
@@ -203,14 +200,26 @@ export default function VideoUpload({
|
|||||||
) : (
|
) : (
|
||||||
<Space size={8} wrap>
|
<Space size={8} wrap>
|
||||||
{cameraEnabled ? (
|
{cameraEnabled ? (
|
||||||
|
// 同「重拍」:透明原生 input 铺满按钮,靠真实手势调起系统相机。
|
||||||
|
<span className="camera-pick-trigger">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept={cameraInputProps.accept}
|
||||||
|
capture={cameraInputProps.capture}
|
||||||
|
className="worker-acceptance-video-camera-input"
|
||||||
|
aria-label="拍摄验收视频"
|
||||||
|
disabled={!canPick}
|
||||||
|
onChange={handleSelect}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<CameraOutlined />}
|
icon={<CameraOutlined />}
|
||||||
disabled={!canPick}
|
disabled={!canPick}
|
||||||
onClick={() => cameraInputRef.current?.click()}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
拍摄验收视频
|
拍摄验收视频
|
||||||
</Button>
|
</Button>
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
icon={<VideoCameraOutlined />}
|
icon={<VideoCameraOutlined />}
|
||||||
|
|||||||
@@ -327,18 +327,33 @@ select {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* 相机 file input 用「移出视口」隐藏,而不是 display:none:
|
* 移动端系统相机入口:
|
||||||
* 部分 iOS Safari 版本不允许对 display:none 的 file input 触发 .click(),会表现为点了没反应。
|
* 原生 input 透明铺满按钮,用户手指直接点在 input 上(真实手势),
|
||||||
|
* 浏览器才更愿意按 capture="environment" 调起相机。
|
||||||
|
*
|
||||||
|
* 旧实现把 input 移到 left:-9999px 再由按钮脚本 .click():
|
||||||
|
* iOS Safari 会点了没反应,部分安卓 WebView 则把相机 intent 降级成文件选择
|
||||||
|
* (表现为点「拍照」却弹出文件浏览)。
|
||||||
*/
|
*/
|
||||||
.image-upload-camera-input,
|
.image-upload-camera-input,
|
||||||
.worker-acceptance-video-camera-input {
|
.worker-acceptance-video-camera-input {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: -9999px;
|
width: 100%;
|
||||||
width: 1px;
|
height: 100%;
|
||||||
height: 1px;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-pick-trigger {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视觉按钮只负责外观,点击统一由上层透明 input 接收(避免脚本 click 触发降级) */
|
||||||
|
.camera-pick-trigger .ant-btn {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export function resolveCameraEntryEnabled(options: ResolveCameraEntryOptions): b
|
|||||||
*
|
*
|
||||||
* `capture="environment"` 让移动浏览器优先打开后置摄像头;iOS Safari 的部分版本会
|
* `capture="environment"` 让移动浏览器优先打开后置摄像头;iOS Safari 的部分版本会
|
||||||
* 忽略该属性并弹出「拍照 / 从相册选择」菜单,属于预期内的降级行为。
|
* 忽略该属性并弹出「拍照 / 从相册选择」菜单,属于预期内的降级行为。
|
||||||
|
*
|
||||||
|
* 用法约束(安卓兼容性):input 必须**透明铺满按钮**、由用户手指直接点到它
|
||||||
|
* (见 base.css 的 `.camera-pick-trigger` / `*-camera-input`)。离屏 + 脚本 `.click()`
|
||||||
|
* 在部分安卓 WebView 上会让浏览器把相机 intent 退化成文件选择。
|
||||||
*/
|
*/
|
||||||
export function cameraCaptureInputProps(kind: CameraCaptureKind): {
|
export function cameraCaptureInputProps(kind: CameraCaptureKind): {
|
||||||
accept: string
|
accept: string
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
# 本地开发网关(仅 dev,生产用 Caddyfile.prod / 其它部署文件):
|
||||||
|
# 管理端:http://<IP>/ 或 http://localhost/
|
||||||
|
# 打手端:http://<IP>:8080/
|
||||||
|
#
|
||||||
|
# 前端是同一份 SPA,靠 hostname 判断站点(apps/frontend/src/utils/site.ts)。
|
||||||
|
# 因此这里用「两个 Vite dev server」而不是只加端口:
|
||||||
|
# :80 → frontend:5173 (其 VITE_WORKER_SITE_URL 指向 worker.localhost,所以 IP 访问=管理端)
|
||||||
|
# :8080 → frontend-worker:5174 (其 VITE_WORKER_SITE_URL 指向 http://<IP>,所以 IP 访问=打手端)
|
||||||
|
# 这样无需改动任何业务代码,生产构建也不受影响。
|
||||||
|
|
||||||
:80 {
|
:80 {
|
||||||
route {
|
route {
|
||||||
handle /api/* {
|
handle /api/* {
|
||||||
@@ -17,3 +27,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:8080 {
|
||||||
|
route {
|
||||||
|
handle /api/* {
|
||||||
|
reverse_proxy backend:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /health* {
|
||||||
|
reverse_proxy backend:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /s/* {
|
||||||
|
reverse_proxy backend:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy frontend-worker:5174
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -152,6 +152,33 @@ services:
|
|||||||
- ./.prettierignore:/.prettierignore:ro
|
- ./.prettierignore:/.prettierignore:ro
|
||||||
- frontend_node_modules:/app/node_modules
|
- frontend_node_modules:/app/node_modules
|
||||||
|
|
||||||
|
# 打手端专用 dev server:同一份代码,只把「打手端 hostname」设成 IP,
|
||||||
|
# 由 Caddy 的 8080 端口反代过来(前端按 hostname 判站点,见 utils/site.ts)。
|
||||||
|
frontend-worker:
|
||||||
|
image: ${BASE_REGISTRY:-docker.m.daocloud.io}/library/node:22-bookworm-slim
|
||||||
|
working_dir: /app
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *json-log-rotation
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
command: >-
|
||||||
|
sh -lc
|
||||||
|
"[ -d node_modules/vite ] || npm install --no-fund --no-audit;
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5174"
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ:-Asia/Shanghai}
|
||||||
|
CHOKIDAR_USEPOLLING: "true"
|
||||||
|
NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
|
||||||
|
VITE_API_TARGET: http://backend:3000
|
||||||
|
VITE_ALLOWED_HOSTS: ${VITE_ALLOWED_HOSTS:-localhost,127.0.0.1}
|
||||||
|
VITE_WORKER_SITE_URL: ${WORKER_SITE_ADDR_IP:-http://192.168.10.8}
|
||||||
|
volumes:
|
||||||
|
- ./apps/frontend:/app
|
||||||
|
- ./.prettierrc.json:/.prettierrc.json:ro
|
||||||
|
- ./.prettierignore:/.prettierignore:ro
|
||||||
|
- frontend_worker_node_modules:/app/node_modules
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: ${BASE_REGISTRY:-docker.m.daocloud.io}/library/caddy:2-alpine
|
image: ${BASE_REGISTRY:-docker.m.daocloud.io}/library/caddy:2-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -161,9 +188,13 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
frontend:
|
frontend:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
frontend-worker:
|
||||||
|
condition: service_started
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
# 打手端(IP 访问):http://<IP>:8080/
|
||||||
|
- "${WORKER_LAN_PORT:-8080}:8080"
|
||||||
environment:
|
environment:
|
||||||
TZ: ${TZ:-Asia/Shanghai}
|
TZ: ${TZ:-Asia/Shanghai}
|
||||||
volumes:
|
volumes:
|
||||||
@@ -174,3 +205,4 @@ volumes:
|
|||||||
minio_dev_data:
|
minio_dev_data:
|
||||||
backend_node_modules:
|
backend_node_modules:
|
||||||
frontend_node_modules:
|
frontend_node_modules:
|
||||||
|
frontend_worker_node_modules:
|
||||||
|
|||||||
Reference in New Issue
Block a user