SWX\10484 6 days ago
commit d4f0babc60
  1. 8
      src/api/login.js
  2. 189
      src/layout/components/ESignatureDialog.vue
  3. 8
      src/utils/auth.js
  4. 26
      src/views/contacts/index.vue

@ -89,3 +89,11 @@ export function postCommonCheckVersion(data) {
data: data data: data
}) })
} }
// 更新签名
export function postUpdateOpsSignatureUpdate(data) {
return request({
url: '/users/ops/signature/update',
method: 'post',
data
})
}

@ -6,7 +6,7 @@
:show-close="true" :show-close="true"
@close="handleClose" @close="handleClose"
@opened="initCanvas" @opened="initCanvas"
qappend-to-body append-to-body
> >
<el-form label-width="80px"> <el-form label-width="80px">
<el-form-item label="签名区域"> <el-form-item label="签名区域">
@ -40,13 +40,16 @@
ref="fileInput" ref="fileInput"
type="file" type="file"
accept="image/*" accept="image/*"
class="file-input" style="display: none"
@change="handleFileChange" @change="handleFileSelect"
/> />
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import { postUpdateOpsSignatureUpdate } from "@/api/login";
import { uploadFile } from "@/utils/requestMinio";
import { setLoginInfo } from "@/utils/auth";
export default { export default {
name: "ESignatureDialog", name: "ESignatureDialog",
data() { data() {
@ -220,45 +223,100 @@ export default {
this.$refs.fileInput.click(); this.$refs.fileInput.click();
}, },
handleFileChange(e) { //
const file = e.target.files[0]; async handleFileSelect(event) {
if (file && this.ctx) { const file = event.target.files[0];
if (!file) return;
try {
await this.loadImageToCanvas(file);
} catch (error) {
console.error("图片加载失败:", error);
this.$modal.msgError("图片加载失败: " + error.message);
} finally {
//
event.target.value = "";
}
},
//
async loadImageToCanvas(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = (e) => {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
const canvas = this.$refs.signatureCanvas; const canvas = this.$refs.signatureCanvas;
const ctx = this.ctx;
// //
this.ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
// //
const scale = Math.min( const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
canvas.width / img.width,
canvas.height / img.height,
1
);
const x = (canvas.width - img.width * scale) / 2; const x = (canvas.width - img.width * scale) / 2;
const y = (canvas.height - img.height * scale) / 2; const y = (canvas.height - img.height * scale) / 2;
this.ctx.drawImage( //
img, ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
x,
y, //
img.width * scale,
img.height * scale
);
this.saveState(); this.saveState();
resolve();
}; };
img.src = event.target.result; img.onerror = () => reject(new Error("图片加载失败"));
img.src = e.target.result;
}; };
reader.onerror = () => reject(new Error("文件读取失败"));
reader.readAsDataURL(file); reader.readAsDataURL(file);
} });
// input
e.target.value = "";
}, },
// MinIO
async uploadAvatar(file) {
try {
// MinIO
await this.ensureMinioInitialized();
const config = this.$store.getters.config;
const bucket = config.MINIO_BUCKET_AVATAR || "remote-avatar-test";
const timestamp = Date.now();
const ext = file.name.split(".").pop() || "png";
const fileName = `${timestamp}.${ext}`;
const objectName = `head_portrait/${fileName}`;
console.log(
`Uploading avatar to bucket: ${bucket}, object: ${objectName}`
);
// 使 MinIO
const result = await uploadFile(
bucket,
objectName,
file,
{},
(percent) => {}
);
console.log("MinIO 上传成功:", result);
let avatar = config.MINIO_BUCKET_AVATAR + "/" + result.objectName;
//
await postUpdateOpsSignatureUpdate({
//
signature: avatar,
//
sign_hash: avatar,
});
//
submit() { } catch (error) {
throw error;
}
},
// MinIO
async ensureMinioInitialized() {
const config = this.$store.getters.config;
if (!config || !config.MINIO_ENDPOINT) {
console.log("MinIO config not loaded, fetching...");
await this.$store.dispatch("GetNetConfig");
}
},
async submit() {
const canvas = this.$refs.signatureCanvas; const canvas = this.$refs.signatureCanvas;
if (!canvas || !this.ctx) return; if (!canvas || !this.ctx) return;
@ -282,10 +340,85 @@ export default {
return; return;
} }
this.signatureData = canvas.toDataURL("image/png"); try {
// blob
const blob = await new Promise((resolve) => {
canvas.toBlob(resolve, "image/png");
});
//
await this.uploadSignatureToServer(blob);
this.$message.success("签名上传成功"); this.$message.success("签名上传成功");
this.$emit("submit", this.signatureData); this.$emit("submit", canvas.toDataURL("image/png"));
this.handleClose(); this.handleClose();
} catch (error) {
console.error("签名上传失败:", error);
this.$message.error("签名上传失败: " + error.message);
}
},
//
async uploadSignatureToServer(blob) {
// MinIO
await this.ensureMinioInitialized();
const config = this.$store.getters.config;
// MINIO_BUCKET_KERNEL/esign/_esign_.png
const bucket = config.MINIO_BUCKET_KERNEL || "remote-kernel-test";
const loginInfo = this.$store.state.user.loginInfo;
const username = loginInfo?.username || "unknown";
const timestamp = Date.now();
const objectName = `esign/${username}_esign_${timestamp}.png`;
console.log(`Uploading signature to bucket: ${bucket}, object: ${objectName}`);
// 使 MinIO
const result = await uploadFile(
bucket,
objectName,
blob,
{},
(percent) => {}
);
console.log("MinIO 上传成功:", result);
//
const signature = `${bucket}/${result.objectName}`;
//
const signHash = await this.calculateFileHash(blob);
//
await postUpdateOpsSignatureUpdate({
signature: signature,
sign_hash: signHash,
});
// loginInfo
this.updateLoginInfoSignature(signature);
},
// loginInfo
updateLoginInfoSignature(signature) {
const loginInfo = { ...this.$store.state.user.loginInfo };
loginInfo.signature = signature;
// Vuex store
this.$store.commit('SET_LOGIN_INFO', loginInfo);
//
try {
setLoginInfo(loginInfo);
} catch (e) {
console.error("更新本地存储失败:", e);
}
},
// SHA-256
async calculateFileHash(blob) {
const arrayBuffer = await blob.arrayBuffer();
const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
return hashHex;
}, },
handleClose() { handleClose() {

@ -5,14 +5,16 @@ const TokenKey = 'utalk-token'
export function getToken() { export function getToken() {
return Cookies.get(TokenKey) return Cookies.get(TokenKey)
} }
// auth.js 修改
export function getLoginInfo() { export function getLoginInfo() {
return Cookies.get('loginInfo') const info = Cookies.get('loginInfo')
return info ? JSON.parse(info) : null // 添加反序列化
} }
export function setToken(token) { export function setToken(token) {
return Cookies.set(TokenKey, token, { expires: 7 }) return Cookies.set(TokenKey, token)
} }
export function setLoginInfo(loginInfo) { export function setLoginInfo(loginInfo) {
return Cookies.set('loginInfo', loginInfo, { expires: 7 }) return Cookies.set('loginInfo', JSON.stringify(loginInfo)) // 添加序列化
} }
export function removeToken() { export function removeToken() {

@ -43,11 +43,7 @@
/> />
</div> </div>
<!-- 滚动事件只在常用联系人场景生效 --> <!-- 滚动事件只在常用联系人场景生效 -->
<div <div v-loading="loading" class="member-list" @scroll="handleScroll">
v-loading="loading"
class="member-list"
@scroll="handleScroll"
>
<div <div
v-for="member in memberList" v-for="member in memberList"
:key="member.id" :key="member.id"
@ -71,7 +67,10 @@
description="暂无成员" description="暂无成员"
/> />
<!-- 分页底部提示仅常用联系人展示 --> <!-- 分页底部提示仅常用联系人展示 -->
<div v-if="isLatestContacts && memberList.length > 0" class="load-more-tip"> <div
v-if="isLatestContacts && memberList.length > 0"
class="load-more-tip"
>
<span v-if="loadMoreLoading">加载更多...</span> <span v-if="loadMoreLoading">加载更多...</span>
<span v-if="noMore && !loadMoreLoading">没有更多数据了</span> <span v-if="noMore && !loadMoreLoading">没有更多数据了</span>
</div> </div>
@ -273,13 +272,15 @@ export default {
resList = data.list || []; resList = data.list || [];
} else { } else {
// //
const { data } = await getGroupsListUser({ group_id: this.currentGroupId }); const { data } = await getGroupsListUser({
group_id: this.currentGroupId,
});
resList = data.list || []; resList = data.list || [];
} }
const formatList = resList.map(item => ({ const formatList = resList.map((item) => ({
...item, ...item,
online: [1, true].includes(item.online) online: [1, true].includes(item.online),
})); }));
if (isRefresh) { if (isRefresh) {
@ -312,7 +313,7 @@ export default {
try { try {
const { data } = await getGroupsList(); const { data } = await getGroupsList();
this.treeData = data.list; this.treeData = data.list;
this.defaultExpandedKeys = this.treeData.map(node => node.id); this.defaultExpandedKeys = this.treeData.map((node) => node.id);
if (!this.userGroupId) return; if (!this.userGroupId) return;
const parentKeys = this.findParentKeys(this.treeData, this.userGroupId); const parentKeys = this.findParentKeys(this.treeData, this.userGroupId);
this.$nextTick(() => { this.$nextTick(() => {
@ -332,7 +333,10 @@ export default {
for (const node of tree) { for (const node of tree) {
if (node.id === targetId) return [...parents, node.id]; if (node.id === targetId) return [...parents, node.id];
if (node.child && node.child.length) { if (node.child && node.child.length) {
const res = this.findParentKeys(node.child, targetId, [...parents, node.id]); const res = this.findParentKeys(node.child, targetId, [
...parents,
node.id,
]);
if (res.length) return res; if (res.length) return res;
} }
} }

Loading…
Cancel
Save