Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xyw_asset_console
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
DaiJiezhang
xyw_asset_console
Commits
755bba6d
Commit
755bba6d
authored
Jul 07, 2026
by
DaiJiezhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refine phone image handling flow
parent
1b367d0d
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
349 additions
and
83 deletions
+349
-83
WxPhoneEntity.java
...main/java/com/xyw/console/phone/entity/WxPhoneEntity.java
+3
-2
WxPhoneMapper.java
...main/java/com/xyw/console/phone/mapper/WxPhoneMapper.java
+12
-0
WxPhoneService.java
...in/java/com/xyw/console/phone/service/WxPhoneService.java
+35
-2
index.html
index.html
+4
-4
phone-image-cell.js
src/modules/phone/phone-image-cell.js
+30
-61
phone-list-runtime.js
src/modules/phone/phone-list-runtime.js
+212
-11
phone-api-client.js
src/modules/shared/phone-api-client.js
+9
-2
styles-v2.css
styles-v2.css
+44
-1
No files found.
backend/src/main/java/com/xyw/console/phone/entity/WxPhoneEntity.java
View file @
755bba6d
package
com
.
xyw
.
console
.
phone
.
entity
;
package
com
.
xyw
.
console
.
phone
.
entity
;
import
com.baomidou.mybatisplus.annotation.FieldStrategy
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableField
;
import
com.baomidou.mybatisplus.annotation.TableField
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableId
;
...
@@ -25,10 +26,10 @@ public class WxPhoneEntity {
...
@@ -25,10 +26,10 @@ public class WxPhoneEntity {
@TableField
(
"city"
)
@TableField
(
"city"
)
private
String
city
;
private
String
city
;
@TableField
(
"image_attachment_1"
)
@TableField
(
value
=
"image_attachment_1"
,
updateStrategy
=
FieldStrategy
.
ALWAYS
)
private
String
imageAttachment1
;
private
String
imageAttachment1
;
@TableField
(
"image_attachment_2"
)
@TableField
(
value
=
"image_attachment_2"
,
updateStrategy
=
FieldStrategy
.
ALWAYS
)
private
String
imageAttachment2
;
private
String
imageAttachment2
;
@TableField
(
"card_status"
)
@TableField
(
"card_status"
)
...
...
backend/src/main/java/com/xyw/console/phone/mapper/WxPhoneMapper.java
View file @
755bba6d
...
@@ -2,6 +2,18 @@ package com.xyw.console.phone.mapper;
...
@@ -2,6 +2,18 @@ package com.xyw.console.phone.mapper;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.xyw.console.phone.entity.WxPhoneEntity
;
import
com.xyw.console.phone.entity.WxPhoneEntity
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.annotations.Update
;
public
interface
WxPhoneMapper
extends
BaseMapper
<
WxPhoneEntity
>
{
public
interface
WxPhoneMapper
extends
BaseMapper
<
WxPhoneEntity
>
{
/**
* 显式更新图片字段,确保 null 值能写入 DB。
* FieldStrategy.ALWAYS 在自定义 SqlSessionFactory 下可能不生效,
* 这里用原生 SQL 绕过,保证删除图片时字段被置为 NULL。
*/
@Update
(
"UPDATE wx_phone SET image_attachment_1 = #{imageAttachment1}, image_attachment_2 = #{imageAttachment2} WHERE id = #{id}"
)
int
updateImages
(
@Param
(
"id"
)
Long
id
,
@Param
(
"imageAttachment1"
)
String
imageAttachment1
,
@Param
(
"imageAttachment2"
)
String
imageAttachment2
);
}
}
backend/src/main/java/com/xyw/console/phone/service/WxPhoneService.java
View file @
755bba6d
...
@@ -6,8 +6,10 @@ import com.xyw.console.phone.entity.WxPhoneEntity;
...
@@ -6,8 +6,10 @@ import com.xyw.console.phone.entity.WxPhoneEntity;
import
com.xyw.console.phone.mapper.WxPhoneMapper
;
import
com.xyw.console.phone.mapper.WxPhoneMapper
;
import
java.util.List
;
import
java.util.List
;
import
lombok.RequiredArgsConstructor
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
@Slf4j
@Service
@Service
@RequiredArgsConstructor
@RequiredArgsConstructor
public
class
WxPhoneService
{
public
class
WxPhoneService
{
...
@@ -41,13 +43,44 @@ public class WxPhoneService {
...
@@ -41,13 +43,44 @@ public class WxPhoneService {
/**
/**
* 浠g爜浣滅敤锛堢櫧璇濓級锛氭洿鏂版寚瀹氭墜鏈哄彿鍗★紝骞舵妸鏁版嵁搴撻噷鐨勬渶鏂拌褰曡繑鍥炵粰鍓嶇銆? * 鍏宠仈鏂囦欢锛欶:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java銆丗:/Project/xyw_console/src/modules/phone/phone-add-dialog.js銆? * 鍏宠仈閫昏緫锛堣皟鐢ㄩ摼/娑堟伅閾?鏁版嵁娴侊級锛歅UT /api/wx-phones/{id} -> updateRecord() -> Mapper.updateById() -> 鍓嶇鍒锋柊鍒楄〃銆? */
* 浠g爜浣滅敤锛堢櫧璇濓級锛氭洿鏂版寚瀹氭墜鏈哄彿鍗★紝骞舵妸鏁版嵁搴撻噷鐨勬渶鏂拌褰曡繑鍥炵粰鍓嶇銆? * 鍏宠仈鏂囦欢锛欶:/Project/xyw_console/backend/src/main/java/com/xyw/console/phone/controller/WxPhoneController.java銆丗:/Project/xyw_console/src/modules/phone/phone-add-dialog.js銆? * 鍏宠仈閫昏緫锛堣皟鐢ㄩ摼/娑堟伅閾?鏁版嵁娴侊級锛歅UT /api/wx-phones/{id} -> updateRecord() -> Mapper.updateById() -> 鍓嶇鍒锋柊鍒楄〃銆? */
public
WxPhoneEntity
updateRecord
(
Long
id
,
WxPhoneSaveRequest
request
)
{
public
WxPhoneEntity
updateRecord
(
Long
id
,
WxPhoneSaveRequest
request
)
{
log
.
info
(
"更新请求:id={}, request.imageAttachment1={}, request.imageAttachment2={}"
,
id
,
request
.
getImageAttachment1
()
==
null
?
"null"
:
"长度="
+
request
.
getImageAttachment1
().
length
(),
request
.
getImageAttachment2
()
==
null
?
"null"
:
"长度="
+
request
.
getImageAttachment2
().
length
());
WxPhoneEntity
entity
=
wxPhoneMapper
.
selectById
(
id
);
WxPhoneEntity
entity
=
wxPhoneMapper
.
selectById
(
id
);
if
(
entity
==
null
)
{
if
(
entity
==
null
)
{
log
.
warn
(
"更新手机号卡失败:未找到 id={}"
,
id
);
return
null
;
return
null
;
}
}
String
oldImage1
=
entity
.
getImageAttachment1
();
String
oldImage2
=
entity
.
getImageAttachment2
();
copyRequestToEntity
(
request
,
entity
);
copyRequestToEntity
(
request
,
entity
);
wxPhoneMapper
.
updateById
(
entity
);
String
newImage1
=
entity
.
getImageAttachment1
();
return
normalizeEntity
(
wxPhoneMapper
.
selectById
(
id
));
String
newImage2
=
entity
.
getImageAttachment2
();
log
.
info
(
"复制后:id={}, entity.imageAttachment1={}, entity.imageAttachment2={}"
,
id
,
newImage1
==
null
?
"null"
:
"长度="
+
newImage1
.
length
(),
newImage2
==
null
?
"null"
:
"长度="
+
newImage2
.
length
());
if
(
oldImage1
!=
null
&&
newImage1
==
null
)
{
log
.
info
(
"删除图片:id={}, 字段=image_attachment_1, 旧值长度={}"
,
id
,
oldImage1
.
length
());
}
if
(
oldImage2
!=
null
&&
newImage2
==
null
)
{
log
.
info
(
"删除图片:id={}, 字段=image_attachment_2, 旧值长度={}"
,
id
,
oldImage2
.
length
());
}
int
updatedRows
=
wxPhoneMapper
.
updateById
(
entity
);
log
.
info
(
"updateById 返回行数={}, id={}"
,
updatedRows
,
id
);
// 显式更新图片字段,绕过 FieldStrategy.ALWAYS 可能不生效的问题,确保 null 值写入 DB
int
imageRows
=
wxPhoneMapper
.
updateImages
(
id
,
newImage1
,
newImage2
);
log
.
info
(
"updateImages 返回行数={}, id={}, image1={}, image2={}"
,
imageRows
,
id
,
newImage1
==
null
?
"null"
:
"长度="
+
newImage1
.
length
(),
newImage2
==
null
?
"null"
:
"长度="
+
newImage2
.
length
());
WxPhoneEntity
updated
=
wxPhoneMapper
.
selectById
(
id
);
log
.
info
(
"更新完成:id={}, image_attachment_1={}, image_attachment_2={}"
,
id
,
updated
.
getImageAttachment1
()
!=
null
?
"存在(长度="
+
updated
.
getImageAttachment1
().
length
()
+
")"
:
"null"
,
updated
.
getImageAttachment2
()
!=
null
?
"存在(长度="
+
updated
.
getImageAttachment2
().
length
()
+
")"
:
"null"
);
return
normalizeEntity
(
updated
);
}
}
/**
/**
...
...
index.html
View file @
755bba6d
<!DOCTYPE html>
<!DOCTYPE html>
<html
lang=
"zh-CN"
>
<html
lang=
"zh-CN"
>
<head>
<head>
...
@@ -7,7 +7,7 @@
...
@@ -7,7 +7,7 @@
<title>
学有为资产管理台
</title>
<title>
学有为资产管理台
</title>
<link
rel=
"icon"
href=
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230f766e'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='26' font-family='Arial' fill='white'%3EXY%3C/text%3E%3C/svg%3E"
/>
<link
rel=
"icon"
href=
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230f766e'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='26' font-family='Arial' fill='white'%3EXY%3C/text%3E%3C/svg%3E"
/>
<link
rel=
"stylesheet"
href=
"node_modules/element-plus/dist/index.css"
/>
<link
rel=
"stylesheet"
href=
"node_modules/element-plus/dist/index.css"
/>
<link
rel=
"stylesheet"
href=
"styles-v2.css"
/>
<link
rel=
"stylesheet"
href=
"styles-v2.css
?v=20260707
"
/>
</head>
</head>
<body>
<body>
...
@@ -91,8 +91,8 @@
...
@@ -91,8 +91,8 @@
<script
src=
"src/modules/wechat/wechat-list-runtime.js"
></script>
<script
src=
"src/modules/wechat/wechat-list-runtime.js"
></script>
<script
src=
"src/modules/phone/phone-add-dialog.js"
></script>
<script
src=
"src/modules/phone/phone-add-dialog.js"
></script>
<script
src=
"src/modules/phone/phone-detail-drawer.js"
></script>
<script
src=
"src/modules/phone/phone-detail-drawer.js"
></script>
<script
src=
"src/modules/phone/phone-image-cell.js"
></script>
<script
src=
"src/modules/phone/phone-image-cell.js
?v=20260707
"
></script>
<script
src=
"src/modules/phone/phone-list-runtime.js"
></script>
<script
src=
"src/modules/phone/phone-list-runtime.js
?v=20260707
"
></script>
<script
src=
"src/modules/shared/record-adapters.js"
></script>
<script
src=
"src/modules/shared/record-adapters.js"
></script>
<script
src=
"src/modules/shared/wechat-api-client.js"
></script>
<script
src=
"src/modules/shared/wechat-api-client.js"
></script>
<script
src=
"src/modules/shared/phone-api-client.js"
></script>
<script
src=
"src/modules/shared/phone-api-client.js"
></script>
...
...
src/modules/phone/phone-image-cell.js
View file @
755bba6d
(
function
attachPhoneImageCell
()
{
(
function
attachPhoneImageCell
()
{
const
{
ref
}
=
window
.
Vue
;
const
{
ref
}
=
window
.
Vue
;
/**
* 代码作用(白话):手机号卡列表图片列的图片单元组件,负责缩略图展示、占位图回退、预览与删除悬浮操作;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):PhoneListTable 图片列 -> <phone-card-image-cell> -> 预览/删除回调 -> 列表刷新。
*/
function
handleImageError
(
event
)
{
function
handleImageError
(
event
)
{
const
image
=
event
?.
target
;
const
image
=
event
?.
target
;
if
(
!
image
)
{
if
(
!
image
)
{
return
;
}
return
;
}
image
.
style
.
display
=
'none'
;
image
.
style
.
display
=
'none'
;
const
placeholder
=
image
.
nextElementSibling
;
const
placeholder
=
image
.
nextElementSibling
;
if
(
placeholder
)
{
if
(
placeholder
)
{
placeholder
.
style
.
display
=
'flex'
;
}
placeholder
.
style
.
display
=
'flex'
;
}
}
}
const
PhoneCardImageCell
=
{
const
PhoneCardImageCell
=
{
...
@@ -21,76 +14,52 @@
...
@@ -21,76 +14,52 @@
props
:
{
props
:
{
images
:
{
type
:
Array
,
default
:
()
=>
[]
},
images
:
{
type
:
Array
,
default
:
()
=>
[]
},
recordId
:
{
type
:
[
String
,
Number
],
default
:
null
},
recordId
:
{
type
:
[
String
,
Number
],
default
:
null
},
clearImage
:
{
type
:
Function
,
default
:
null
}
clearImage
:
{
type
:
Function
,
default
:
null
},
uploadImage
:
{
type
:
Function
,
default
:
null
}
},
},
setup
(
props
)
{
setup
(
props
)
{
const
previewVisible
=
ref
(
false
);
const
previewVisible
=
ref
(
false
);
const
previewIndex
=
ref
(
0
);
const
previewIndex
=
ref
(
0
);
const
imageInputRef
=
ref
(
null
);
const
uploading
=
ref
(
false
);
function
openPreview
(
index
)
{
function
openPreview
(
index
)
{
previewIndex
.
value
=
index
;
previewVisible
.
value
=
true
;
}
previewIndex
.
value
=
index
;
function
closePreview
()
{
previewVisible
.
value
=
false
;
}
previewVisible
.
value
=
true
;
function
handleDelete
(
index
)
{
if
(
typeof
props
.
clearImage
===
'function'
)
{
props
.
clearImage
(
index
);
}
}
}
function
closePreview
()
{
function
triggerUpload
()
{
previewVisible
.
value
=
false
;
if
(
uploading
.
value
)
{
return
;
}
if
(
typeof
props
.
uploadImage
!==
'function'
)
{
return
;
}
imageInputRef
.
value
?.
click
();
}
}
function
handleDelete
(
index
)
{
async
function
handleFileChange
(
event
)
{
if
(
typeof
props
.
clearImage
===
'function'
)
{
const
input
=
event
?.
target
;
props
.
clearImage
(
index
);
const
files
=
Array
.
from
(
input
?.
files
||
[]);
}
if
(
!
files
.
length
)
{
if
(
input
)
{
input
.
value
=
''
;
}
return
;
}
uploading
.
value
=
true
;
try
{
await
props
.
uploadImage
(
files
);
}
finally
{
uploading
.
value
=
false
;
if
(
input
)
{
input
.
value
=
''
;
}
}
}
}
return
{
return
{
previewVisible
,
previewIndex
,
imageInputRef
,
openPreview
,
closePreview
,
handleDelete
,
handleImageError
,
triggerUpload
,
handleFileChange
};
previewVisible
,
previewIndex
,
openPreview
,
closePreview
,
handleDelete
,
handleImageError
};
},
},
template
:
`
template
:
`
<div class="phone-image-cell">
<div class="phone-image-cell" @click.stop @mousedown.stop @dblclick.stop>
<input ref="imageInputRef" type="file" accept="image/*" multiple class="phone-image-input" @change="handleFileChange" style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;" />
<template v-if="images && images.length">
<template v-if="images && images.length">
<div
<div v-for="(image, index) in images" :key="(recordId ?? 'row') + '-img-' + index" class="phone-image-thumb" @click.stop @mousedown.stop @dblclick.stop>
v-for="(image, index) in images"
<img :src="image" alt="thumbnail" class="phone-image-img" @error="handleImageError" />
:key="(recordId ?? 'row') + '-img-' + index"
<div class="phone-image-fallback">双击上传</div>
class="phone-image-thumb"
>
<img :src="image" alt="号卡图片缩略图" class="phone-image-img" @error="handleImageError" />
<div class="phone-image-fallback">占位图</div>
<div class="phone-image-actions" @click.stop @mousedown.stop @dblclick.stop>
<div class="phone-image-actions" @click.stop @mousedown.stop @dblclick.stop>
<button
<button type="button" class="phone-image-action phone-image-action--preview" title="预览" @click.stop="openPreview(index)" @mousedown.stop @dblclick.stop><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M515.2 224c-307.2 0-492.8 313.6-492.8 313.6s214.4 304 492.8 304 492.8-304 492.8-304S822.4 224 515.2 224zM832 652.8c-102.4 86.4-211.2 140.8-320 140.8s-217.6-51.2-320-140.8c-35.2-32-70.4-64-99.2-99.2-6.4-6.4-9.6-12.8-16-19.2 3.2-6.4 9.6-12.8 12.8-19.2 25.6-35.2 57.6-70.4 92.8-102.4 99.2-89.6 208-144 329.6-144s230.4 54.4 329.6 144c35.2 32 64 67.2 92.8 102.4 3.2 6.4 9.6 12.8 12.8 19.2-3.2 6.4-9.6 12.8-16 19.2C902.4 585.6 870.4 620.8 832 652.8z"/><path fill="currentColor" d="M512 345.6c-96 0-169.6 76.8-169.6 169.6 0 96 76.8 169.6 169.6 169.6 96 0 169.6-76.8 169.6-169.6C681.6 422.4 604.8 345.6 512 345.6zM512 640c-67.2 0-121.6-54.4-121.6-121.6 0-67.2 54.4-121.6 121.6-121.6 67.2 0 121.6 54.4 121.6 121.6C633.6 582.4 579.2 640 512 640z"/></svg></button>
type="button"
<button type="button" class="phone-image-action phone-image-action--delete" title="删除" @click.stop="handleDelete(index)" @mousedown.stop @dblclick.stop><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M709.469091 209.454545H930.909091a34.909091 34.909091 0 0 1 0 69.818182h-81.454546v607.185455c0 56.366545-44.311273 102.632727-99.746909 102.632727H274.292364c-55.435636 0-99.746909-46.266182-99.746909-102.632727V279.272727H93.090909a34.909091 34.909091 0 0 1 0-69.818182h244.712727a186.181818 186.181818 0 0 1 371.665455 0z m-70.050909 0a116.363636 116.363636 0 0 0-231.563637 0h231.563637z m140.218182 69.818182h-535.272728v607.185455c0 18.455273 13.730909 32.814545 29.928728 32.814545h475.415272c16.174545 0 29.928727-14.359273 29.928728-32.814545V279.272727z m-418.909091 147.2a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z m232.727272 0a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z"/></svg></button>
class="phone-image-action phone-image-action--preview"
title="预览图片"
@click.stop="openPreview(index)"
@mousedown.stop
><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M515.2 224c-307.2 0-492.8 313.6-492.8 313.6s214.4 304 492.8 304 492.8-304 492.8-304S822.4 224 515.2 224zM832 652.8c-102.4 86.4-211.2 140.8-320 140.8s-217.6-51.2-320-140.8c-35.2-32-70.4-64-99.2-99.2-6.4-6.4-9.6-12.8-16-19.2 3.2-6.4 9.6-12.8 12.8-19.2 25.6-35.2 57.6-70.4 92.8-102.4 99.2-89.6 208-144 329.6-144s230.4 54.4 329.6 144c35.2 32 64 67.2 92.8 102.4 3.2 6.4 9.6 12.8 12.8 19.2-3.2 6.4-9.6 12.8-16 19.2C902.4 585.6 870.4 620.8 832 652.8z"/><path fill="currentColor" d="M512 345.6c-96 0-169.6 76.8-169.6 169.6 0 96 76.8 169.6 169.6 169.6 96 0 169.6-76.8 169.6-169.6C681.6 422.4 604.8 345.6 512 345.6zM512 640c-67.2 0-121.6-54.4-121.6-121.6 0-67.2 54.4-121.6 121.6-121.6 67.2 0 121.6 54.4 121.6 121.6C633.6 582.4 579.2 640 512 640z"/></svg></button>
<button
type="button"
class="phone-image-action phone-image-action--delete"
title="删除图片"
@click.stop="handleDelete(index)"
@mousedown.stop
><svg viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M709.469091 209.454545H930.909091a34.909091 34.909091 0 0 1 0 69.818182h-81.454546v607.185455c0 56.366545-44.311273 102.632727-99.746909 102.632727H274.292364c-55.435636 0-99.746909-46.266182-99.746909-102.632727V279.272727H93.090909a34.909091 34.909091 0 0 1 0-69.818182h244.712727a186.181818 186.181818 0 0 1 371.665455 0z m-70.050909 0a116.363636 116.363636 0 0 0-231.563637 0h231.563637z m140.218182 69.818182h-535.272728v607.185455c0 18.455273 13.730909 32.814545 29.928728 32.814545h475.415272c16.174545 0 29.928727-14.359273 29.928728-32.814545V279.272727z m-418.909091 147.2a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z m232.727272 0a34.909091 34.909091 0 0 1 69.818182 0v338.897455a34.909091 34.909091 0 0 1-69.818182 0V426.472727z"/></svg></button>
</div>
</div>
</div>
</div>
</template>
</template>
<div v-else class="phone-image-thumb phone-image-thumb--empty">
<div v-else class="phone-image-thumb phone-image-thumb--empty"
title="双击上传" @dblclick.stop="triggerUpload" @mousedown.stop @click.stop
>
<span class="phone-image-fallback">
占位图
</span>
<span class="phone-image-fallback">
双击上传
</span>
</div>
</div>
<el-image-viewer
<el-image-viewer v-if="previewVisible" :url-list="images" :initial-index="previewIndex" :z-index="3000" :hide-on-click-modal="true" teleported @close="closePreview" />
v-if="previewVisible"
:url-list="images"
:initial-index="previewIndex"
:z-index="3000"
:hide-on-click-modal="true"
teleported
@close="closePreview"
/>
</div>
</div>
`
`
};
};
...
...
src/modules/phone/phone-list-runtime.js
View file @
755bba6d
...
@@ -86,6 +86,94 @@
...
@@ -86,6 +86,94 @@
const
multipleTableRef
=
ref
(
null
);
const
multipleTableRef
=
ref
(
null
);
/**
/**
* 代码作用(白话):内联编辑状态,记录当前正在编辑的单元格 { recordId, field, value },同一时刻只允许一个单元格处于编辑态;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):双击单元格 -> handleCellDblclick() -> editingCell -> 渲染输入控件 -> 失焦/回车 -> handleInlineSave() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords()。
*/
const
editingCell
=
ref
(
null
);
/**
* 代码作用(白话):定义哪些字段支持双击内联编辑以及对应的控件类型,避免模板里写死判断逻辑;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):handleCellDblclick() -> isFieldEditable() -> 渲染 input/select/switch。
*/
const
editableFields
=
{
realPerson
:
{
type
:
'input'
,
placeholder
:
'请输入实名人'
},
iccid
:
{
type
:
'input'
,
placeholder
:
'请输入 ICCID'
},
city
:
{
type
:
'input'
,
placeholder
:
'请输入城市'
},
cardStatus
:
{
type
:
'select'
,
options
:
[
'正常'
,
'异常停机'
,
'保号中'
]
},
carrier
:
{
type
:
'select'
,
options
:
[
'中国移动'
,
'中国联通'
,
'中国电信'
,
'中国广电'
,
'亮哥渠道(虚拟)'
]
},
outbound
:
{
type
:
'select'
,
options
:
[
'可外呼'
,
'不可外呼'
]
},
douyinAccount
:
{
type
:
'input'
,
placeholder
:
'请输入抖音账号'
},
linkedWecom
:
{
type
:
'input'
,
placeholder
:
'请输入绑定企业微信'
},
channelOperator
:
{
type
:
'input'
,
placeholder
:
'请输入渠道运营商'
}
};
/**
* 代码作用(白话):判断某个字段是否允许双击内联编辑;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):模板 @dblclick -> isFieldEditable() -> 决定是否进入编辑态。
*/
function
isFieldEditable
(
field
)
{
return
Object
.
prototype
.
hasOwnProperty
.
call
(
editableFields
,
field
);
}
/**
* 代码作用(白话):双击单元格时进入编辑态,把当前值暂存到 editingCell,阻止事件冒泡避免触发行选中;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):@dblclick.stop -> handleCellDblclick() -> editingCell = { recordId, field, value }。
*/
function
handleCellDblclick
(
record
,
field
)
{
if
(
!
isFieldEditable
(
field
))
{
return
;
}
let
value
;
if
(
field
===
'outbound'
)
{
value
=
record
.
outboundCall
?
'可外呼'
:
'不可外呼'
;
}
else
{
value
=
record
[
field
]
??
''
;
}
editingCell
.
value
=
{
recordId
:
record
.
id
,
field
,
value
:
typeof
value
===
'boolean'
?
value
:
String
(
value
)
};
}
/**
* 代码作用(白话):内联编辑确认后构造完整保存载荷并调用保存接口,成功后刷新列表并退出编辑态;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):回车/失焦 -> handleInlineSave() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords() -> editingCell = null。
*/
async
function
handleInlineSave
()
{
if
(
!
editingCell
.
value
)
{
return
;
}
const
{
recordId
,
field
,
value
}
=
editingCell
.
value
;
const
record
=
allRecordsCache
.
value
.
find
((
item
)
=>
item
.
id
===
recordId
);
if
(
!
record
)
{
editingCell
.
value
=
null
;
return
;
}
const
updatedRecord
=
{
...
record
};
if
(
field
===
'outbound'
)
{
updatedRecord
.
outboundCall
=
value
===
'可外呼'
;
}
else
{
updatedRecord
[
field
]
=
value
;
}
const
images
=
getDisplayImages
(
record
);
const
payload
=
buildSavePayload
(
updatedRecord
,
images
);
try
{
await
props
.
savePhoneRecord
(
payload
);
if
(
window
.
ElementPlus
?.
ElMessage
)
{
window
.
ElementPlus
.
ElMessage
.
success
(
'修改成功'
);
}
await
loadAllRecords
();
}
catch
(
error
)
{
props
.
notifyError
(
error
.
message
||
'保存失败,请稍后重试'
);
}
finally
{
editingCell
.
value
=
null
;
}
}
/**
* 代码作用(白话):取消内联编辑,直接退出编辑态不保存;关联文件:F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js;关联逻辑(调用链/消息链/数据流):Esc/点击外部 -> handleInlineCancel() -> editingCell = null。
*/
function
handleInlineCancel
()
{
editingCell
.
value
=
null
;
}
/**
* 代码作用(白话):从当前缓存里提取运营商选项,避免筛选下拉框写死,后端换数据时前端还能跟着显示;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):loadAllRecords() -> allRecordsCache -> getCarrierOptions() -> 运营商筛选下拉。
* 代码作用(白话):从当前缓存里提取运营商选项,避免筛选下拉框写死,后端换数据时前端还能跟着显示;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):loadAllRecords() -> allRecordsCache -> getCarrierOptions() -> 运营商筛选下拉。
*/
*/
function
getCarrierOptions
()
{
function
getCarrierOptions
()
{
...
@@ -349,20 +437,95 @@
...
@@ -349,20 +437,95 @@
if
(
!
window
.
confirm
(
'确认删除这张图片吗?'
))
{
if
(
!
window
.
confirm
(
'确认删除这张图片吗?'
))
{
return
;
return
;
}
}
const
remaining
=
getDisplayImages
(
record
).
filter
((
_
,
idx
)
=>
idx
!==
imageIndex
);
// 优先从缓存取最新记录,避免 scope.row 是旧引用导致图片数据不一致
const
payload
=
buildSavePayload
(
record
,
remaining
);
const
latestRecord
=
allRecordsCache
.
value
.
find
((
item
)
=>
item
.
id
===
record
.
id
)
||
record
;
const
currentImages
=
getDisplayImages
(
latestRecord
);
const
updatedImages
=
currentImages
.
slice
();
updatedImages
[
imageIndex
]
=
''
;
const
payload
=
buildSavePayload
(
latestRecord
,
updatedImages
);
const
expectedCount
=
Math
.
max
(
0
,
currentImages
.
filter
(
Boolean
).
length
-
1
);
console
.
info
(
'[phone] 删除图片请求:id='
,
latestRecord
.
id
,
'索引='
,
imageIndex
,
'当前图片数='
,
currentImages
.
filter
(
Boolean
).
length
,
'payload.imageAttachment1 长度='
,
(
payload
.
imageAttachment1
||
''
).
length
,
'payload.imageAttachment2 长度='
,
(
payload
.
imageAttachment2
||
''
).
length
);
try
{
try
{
await
props
.
savePhoneRecord
(
payload
);
const
saved
=
await
props
.
savePhoneRecord
(
payload
);
const
savedImages
=
[
saved
?.
imageAttachment1
||
saved
?.
image_attachment_1
,
saved
?.
imageAttachment2
||
saved
?.
image_attachment_2
].
filter
(
Boolean
);
if
(
savedImages
.
length
>
expectedCount
)
{
console
.
error
(
'[phone] 图片删除验证失败:id='
,
latestRecord
.
id
,
'预期剩余'
,
expectedCount
,
'张,实际'
,
savedImages
.
length
,
'张'
,
'saved.imageAttachment1 长度='
,
(
saved
?.
imageAttachment1
||
saved
?.
image_attachment_1
||
''
).
length
,
'saved.imageAttachment2 长度='
,
(
saved
?.
imageAttachment2
||
saved
?.
image_attachment_2
||
''
).
length
);
props
.
notifyError
(
'图片删除失败:服务端未清除图片数据,请稍后重试'
);
await
loadAllRecords
();
return
;
}
console
.
info
(
'[phone] 图片删除成功:id='
,
latestRecord
.
id
,
'索引='
,
imageIndex
,
'剩余'
,
savedImages
.
length
,
'张'
);
if
(
window
.
ElementPlus
?.
ElMessage
)
{
if
(
window
.
ElementPlus
?.
ElMessage
)
{
window
.
ElementPlus
.
ElMessage
.
success
(
'图片已删除'
);
window
.
ElementPlus
.
ElMessage
.
success
(
'图片已删除'
);
}
}
await
loadAllRecords
();
await
loadAllRecords
();
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'[phone] 删除图片失败:'
,
error
);
props
.
notifyError
(
error
.
message
||
'删除图片失败,请稍后重试'
);
props
.
notifyError
(
error
.
message
||
'删除图片失败,请稍后重试'
);
}
}
}
}
/**
/**
* 代码作用(白话):把本地图片文件读成 base64 DataURL,供列表内联上传直接复用现有保存接口;关联文件:F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):File -> readFileAsDataUrl() -> handleInlineImageUpload() -> buildSavePayload() -> props.savePhoneRecord()。
*/
function
readFileAsDataUrl
(
file
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
reader
=
new
FileReader
();
reader
.
onload
=
()
=>
resolve
(
typeof
reader
.
result
===
'string'
?
reader
.
result
:
''
);
reader
.
onerror
=
()
=>
reject
(
new
Error
(
'读取图片失败,请重新选择'
));
reader
.
readAsDataURL
(
file
);
});
}
/**
* 代码作用(白话):列表空占位框双击后接收用户选择的图片文件,最多保留 2 张,复用全量保存接口写入 imageAttachment1/2,并从后端刷新缓存以保证后续删除能拿到真实 URL;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-image-cell.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):phone-image-cell handleFileChange() -> handleInlineImageUpload() -> readFileAsDataUrl() -> buildSavePayload() -> props.savePhoneRecord() -> loadAllRecords()。
*/
async
function
handleInlineImageUpload
(
record
,
files
)
{
if
(
!
record
||
!
Array
.
isArray
(
files
)
||
!
files
.
length
)
{
return
;
}
// 优先从缓存取最新记录,避免 scope.row 是旧引用导致已有图片丢失
const
latestRecord
=
allRecordsCache
.
value
.
find
((
item
)
=>
item
.
id
===
record
.
id
)
||
record
;
const
existing
=
getDisplayImages
(
latestRecord
).
slice
();
const
remainSlots
=
Math
.
max
(
0
,
2
-
existing
.
length
);
if
(
remainSlots
<=
0
)
{
if
(
window
.
ElementPlus
?.
ElMessage
)
{
window
.
ElementPlus
.
ElMessage
.
warning
(
'最多上传 2 张图片'
);
}
return
;
}
const
picked
=
files
.
slice
(
0
,
remainSlots
);
try
{
const
nextImages
=
existing
.
slice
();
for
(
let
index
=
0
;
index
<
picked
.
length
;
index
+=
1
)
{
const
url
=
await
readFileAsDataUrl
(
picked
[
index
]);
if
(
url
)
{
nextImages
.
push
(
url
);
}
}
if
(
!
nextImages
.
length
)
{
return
;
}
const
payload
=
buildSavePayload
(
latestRecord
,
nextImages
.
slice
(
0
,
2
));
await
props
.
savePhoneRecord
(
payload
);
if
(
window
.
ElementPlus
?.
ElMessage
)
{
window
.
ElementPlus
.
ElMessage
.
success
(
'图片已上传'
);
}
await
loadAllRecords
();
}
catch
(
error
)
{
props
.
notifyError
(
error
.
message
||
'上传图片失败,请稍后重试'
);
}
}
/**
* 代码作用(白话):把某一行设为当前选中手机号,并同步到壳层全局状态,方便详情抽屉和编辑弹窗拿到当前对象;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):点击表格手机号/操作按钮 -> focusRow() -> globalState.selectedBySource.phone -> openDrawer()/详情联动。
* 代码作用(白话):把某一行设为当前选中手机号,并同步到壳层全局状态,方便详情抽屉和编辑弹窗拿到当前对象;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-detail-drawer.js;关联逻辑(调用链/消息链/数据流):点击表格手机号/操作按钮 -> focusRow() -> globalState.selectedBySource.phone -> openDrawer()/详情联动。
*/
*/
function
focusRow
(
recordId
)
{
function
focusRow
(
recordId
)
{
...
@@ -473,6 +636,13 @@
...
@@ -473,6 +636,13 @@
getDisplayImages
,
getDisplayImages
,
handleImageError
,
handleImageError
,
handleClearImage
,
handleClearImage
,
handleInlineImageUpload
,
editingCell
,
editableFields
,
isFieldEditable
,
handleCellDblclick
,
handleInlineSave
,
handleInlineCancel
,
focusRow
,
focusRow
,
resetTableState
,
resetTableState
,
handleRowAction
,
handleRowAction
,
...
@@ -583,6 +753,7 @@
...
@@ -583,6 +753,7 @@
:images="getDisplayImages(scope.row)"
:images="getDisplayImages(scope.row)"
:record-id="scope.row.id"
:record-id="scope.row.id"
:clear-image="(imageIndex) => handleClearImage(scope.row, imageIndex)"
:clear-image="(imageIndex) => handleClearImage(scope.row, imageIndex)"
:upload-image="(files) => handleInlineImageUpload(scope.row, files)"
/>
/>
</template>
</template>
</el-table-column>
</el-table-column>
...
@@ -593,23 +764,37 @@
...
@@ -593,23 +764,37 @@
</el-table-column>
</el-table-column>
<el-table-column label="实名人" min-width="150">
<el-table-column label="实名人" min-width="150">
<template #default="scope">
<template #default="scope">
<span :title="scope.row.realPerson || '-'" style="white-space: nowrap;">{{ scope.row.realPerson || '-' }}</span>
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'realPerson'">
<el-input v-model="editingCell.value" size="small" class="phone-inline-edit-cell" :placeholder="editableFields.realPerson.placeholder" @blur="handleInlineSave" @keyup.enter="handleInlineSave" @keyup.escape="handleInlineCancel" @click.stop />
</template>
<span v-else :title="scope.row.realPerson || '双击编辑'" style="white-space: nowrap; cursor: text;" @dblclick.stop="handleCellDblclick(scope.row, 'realPerson')">{{ scope.row.realPerson || '双击编辑' }}</span>
</template>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="ICCID" min-width="220">
<el-table-column label="ICCID" min-width="220">
<template #default="scope">
<template #default="scope">
<span :title="scope.row.iccid || '-'" style="white-space: nowrap;">{{ scope.row.iccid || '-' }}</span>
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'iccid'">
<el-input v-model="editingCell.value" size="small" class="phone-inline-edit-cell" :placeholder="editableFields.iccid.placeholder" @blur="handleInlineSave" @keyup.enter="handleInlineSave" @keyup.escape="handleInlineCancel" @click.stop />
</template>
<span v-else :title="scope.row.iccid || '双击编辑'" style="white-space: nowrap; cursor: text;" @dblclick.stop="handleCellDblclick(scope.row, 'iccid')">{{ scope.row.iccid || '双击编辑' }}</span>
</template>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="所在城市" min-width="140">
<el-table-column label="所在城市" min-width="140">
<template #default="scope">
<template #default="scope">
<span :title="scope.row.city || '-'" style="white-space: nowrap;">{{ scope.row.city || '-' }}</span>
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'city'">
<el-input v-model="editingCell.value" size="small" class="phone-inline-edit-cell" :placeholder="editableFields.city.placeholder" @blur="handleInlineSave" @keyup.enter="handleInlineSave" @keyup.escape="handleInlineCancel" @click.stop />
</template>
<span v-else :title="scope.row.city || '双击编辑'" style="white-space: nowrap; cursor: text;" @dblclick.stop="handleCellDblclick(scope.row, 'city')">{{ scope.row.city || '双击编辑' }}</span>
</template>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="号卡状态" min-width="120">
<el-table-column label="号卡状态" min-width="120">
<template #default="scope">
<template #default="scope">
<span class="status-chip-wrap">
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'cardStatus'">
<el-select v-model="editingCell.value" size="small" class="phone-inline-edit-cell" @change="handleInlineSave" @click.stop>
<el-option v-for="opt in editableFields.cardStatus.options" :key="opt" :label="opt" :value="opt"></el-option>
</el-select>
</template>
<span v-else class="status-chip-wrap" style="cursor: pointer;" @dblclick.stop="handleCellDblclick(scope.row, 'cardStatus')">
<span class="status-dot" :class="getStatusClass(scope.row.cardStatus || scope.row.card_status || scope.row.status)"></span>
<span class="status-dot" :class="getStatusClass(scope.row.cardStatus || scope.row.card_status || scope.row.status)"></span>
<span>{{ scope.row.cardStatus || scope.row.card_status || scope.row.status }}</span>
<span>{{ scope.row.cardStatus || scope.row.card_status || scope.row.status }}</span>
</span>
</span>
...
@@ -621,11 +806,21 @@
...
@@ -621,11 +806,21 @@
<template #default="scope">{{ formatBooleanText(scope.row.wechat, '已开通', '未开通') }}</template>
<template #default="scope">{{ formatBooleanText(scope.row.wechat, '已开通', '未开通') }}</template>
</el-table-column>
</el-table-column>
<el-table-column label="可外呼" min-width="100">
<el-table-column label="可外呼" min-width="100">
<template #default="scope">{{ formatBooleanText(scope.row.outboundCall, '可外呼', '不可外呼') }}</template>
<template #default="scope">
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'outbound'">
<el-select v-model="editingCell.value" size="small" class="phone-inline-edit-cell" @change="handleInlineSave" @click.stop>
<el-option v-for="opt in editableFields.outbound.options" :key="opt" :label="opt" :value="opt"></el-option>
</el-select>
</template>
<span v-else style="cursor: pointer;" @dblclick.stop="handleCellDblclick(scope.row, 'outbound')">{{ formatBooleanText(scope.row.outboundCall, '可外呼', '不可外呼') }}</span>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="抖音账号" min-width="180">
<el-table-column label="抖音账号" min-width="180">
<template #default="scope">
<template #default="scope">
<span :title="scope.row.douyinAccount || '-'" style="white-space: nowrap;">{{ scope.row.douyinAccount || '-' }}</span>
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'douyinAccount'">
<el-input v-model="editingCell.value" size="small" class="phone-inline-edit-cell" :placeholder="editableFields.douyinAccount.placeholder" @blur="handleInlineSave" @keyup.enter="handleInlineSave" @keyup.escape="handleInlineCancel" @click.stop />
</template>
<span v-else :title="scope.row.douyinAccount || '双击编辑'" style="white-space: nowrap; cursor: text;" @dblclick.stop="handleCellDblclick(scope.row, 'douyinAccount')">{{ scope.row.douyinAccount || '双击编辑' }}</span>
</template>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="小程序备案" min-width="120">
<el-table-column label="小程序备案" min-width="120">
...
@@ -634,7 +829,10 @@
...
@@ -634,7 +829,10 @@
<el-table-column label="渠道运营商" min-width="170">
<el-table-column label="渠道运营商" min-width="170">
<template #default="scope">
<template #default="scope">
<span :title="scope.row.channelOperator || '-'" style="white-space: nowrap;">{{ scope.row.channelOperator || '-' }}</span>
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'channelOperator'">
<el-input v-model="editingCell.value" size="small" class="phone-inline-edit-cell" :placeholder="editableFields.channelOperator.placeholder" @blur="handleInlineSave" @keyup.enter="handleInlineSave" @keyup.escape="handleInlineCancel" @click.stop />
</template>
<span v-else :title="scope.row.channelOperator || '双击编辑'" style="white-space: nowrap; cursor: text;" @dblclick.stop="handleCellDblclick(scope.row, 'channelOperator')">{{ scope.row.channelOperator || '双击编辑' }}</span>
</template>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="号码状态" min-width="120">
<el-table-column label="号码状态" min-width="120">
...
@@ -642,7 +840,10 @@
...
@@ -642,7 +840,10 @@
</el-table-column>
</el-table-column>
<el-table-column label="绑定企业微信" min-width="260">
<el-table-column label="绑定企业微信" min-width="260">
<template #default="scope">
<template #default="scope">
<span :title="scope.row.linkedWecom || '-'" style="white-space: nowrap;">{{ scope.row.linkedWecom || '-' }}</span>
<template v-if="editingCell && editingCell.recordId === scope.row.id && editingCell.field === 'linkedWecom'">
<el-input v-model="editingCell.value" size="small" class="phone-inline-edit-cell" :placeholder="editableFields.linkedWecom.placeholder" @blur="handleInlineSave" @keyup.enter="handleInlineSave" @keyup.escape="handleInlineCancel" @click.stop />
</template>
<span v-else :title="scope.row.linkedWecom || '双击编辑'" style="white-space: nowrap; cursor: text;" @dblclick.stop="handleCellDblclick(scope.row, 'linkedWecom')">{{ scope.row.linkedWecom || '双击编辑' }}</span>
</template>
</template>
</el-table-column> <el-table-column label="操作" width="100" fixed="right">
</el-table-column> <el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<template #default="scope">
...
...
src/modules/shared/phone-api-client.js
View file @
755bba6d
(
function
attachPhoneApiClient
()
{
(
function
attachPhoneApiClient
()
{
const
BASE_URL
=
'http://localhost:8080/api/wx-phones'
;
const
BASE_URL
=
'http://localhost:8080/api/wx-phones'
;
/**
/**
...
@@ -50,12 +50,19 @@
...
@@ -50,12 +50,19 @@
const
payload
=
{
...
record
};
const
payload
=
{
...
record
};
const
isEdit
=
Boolean
(
payload
.
id
);
const
isEdit
=
Boolean
(
payload
.
id
);
const
targetUrl
=
isEdit
?
`
${
BASE_URL
}
/
${
payload
.
id
}
`
:
BASE_URL
;
const
targetUrl
=
isEdit
?
`
${
BASE_URL
}
/
${
payload
.
id
}
`
:
BASE_URL
;
console
.
info
(
'[phone-api] savePhone 请求:id='
,
payload
.
id
,
'imageAttachment1 长度='
,
(
payload
.
imageAttachment1
||
''
).
length
,
'imageAttachment2 长度='
,
(
payload
.
imageAttachment2
||
''
).
length
);
const
json
=
await
requestJson
(
targetUrl
,
{
const
json
=
await
requestJson
(
targetUrl
,
{
method
:
isEdit
?
'PUT'
:
'POST'
,
method
:
isEdit
?
'PUT'
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
(
payload
)
body
:
JSON
.
stringify
(
payload
)
},
'手机号保存失败,请确认本地后端已启动'
);
},
'手机号保存失败,请确认本地后端已启动'
);
return
json
.
data
||
json
;
const
data
=
json
.
data
||
json
;
console
.
info
(
'[phone-api] savePhone 响应:id='
,
payload
.
id
,
'imageAttachment1 长度='
,
(
data
?.
imageAttachment1
||
data
?.
image_attachment_1
||
''
).
length
,
'imageAttachment2 长度='
,
(
data
?.
imageAttachment2
||
data
?.
image_attachment_2
||
''
).
length
);
return
data
;
}
}
/**
/**
...
...
styles-v2.css
View file @
755bba6d
...
@@ -1274,10 +1274,14 @@
...
@@ -1274,10 +1274,14 @@
width
:
100%
;
width
:
100%
;
height
:
100%
;
height
:
100%
;
font-size
:
11px
;
font-size
:
11px
;
color
:
#
94a3b8
;
color
:
#
BBBFCF
!important
;
background
:
#f8fafc
;
background
:
#f8fafc
;
}
}
.phone-table-header-nowrap
span
[
title
=
"双击编辑"
]
{
color
:
#BBBFCF
!important
;
}
.phone-image-thumb--empty
.phone-image-fallback
{
.phone-image-thumb--empty
.phone-image-fallback
{
display
:
flex
;
display
:
flex
;
}
}
...
@@ -1329,3 +1333,42 @@
...
@@ -1329,3 +1333,42 @@
color
:
#ef4444
;
color
:
#ef4444
;
background
:
rgba
(
255
,
255
,
255
,
0.18
);
background
:
rgba
(
255
,
255
,
255
,
0.18
);
}
}
/* Hidden file input used by the phone image cell for double-click upload */
.phone-image-input
{
display
:
none
;
}
/* Empty placeholder: hint interactivity on hover and double-click */
.phone-image-thumb--empty
{
cursor
:
pointer
;
user-select
:
none
;
}
.phone-image-thumb--empty
:hover
{
border-color
:
#BBBFCF
;
background
:
linear-gradient
(
135deg
,
#f1f5f9
0%
,
#e2e8f0
100%
);
}
/* Inline edit cell: input/select used when double-clicking an editable table cell */
.phone-inline-edit-cell
{
width
:
100%
;
min-width
:
80px
;
}
.phone-inline-edit-cell
.el-input__wrapper
,
.phone-inline-edit-cell
.el-select
.el-input__wrapper
{
border-color
:
#BBBFCF
!important
;
box-shadow
:
none
!important
;
background
:
#ffffff
;
}
.phone-inline-edit-cell
.el-input__wrapper
:hover
,
.phone-inline-edit-cell
.el-select
.el-input__wrapper
:hover
{
border-color
:
#9ca3af
!important
;
}
.phone-inline-edit-cell
.el-input__inner
{
font-size
:
13px
;
color
:
#111827
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment