+
+
+
+
+
+
@@ -125,65 +99,93 @@
// ============================================================================
// 导入
// ============================================================================
-import { onMounted, onUnmounted } from 'vue'
+import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
-import { useQrcodeLogin } from '@/composables/useQrcodeLogin'
+import type { FormInstance, FormRules } from 'element-plus'
+import { User, Lock, Key } from '@element-plus/icons-vue'
+import { useAgentStore } from '@/stores/agent'
// ============================================================================
// 状态
// ============================================================================
const router = useRouter()
+const agentStore = useAgentStore()
-/**
- * 扫码登录成功回调
- * 1. 存 token 到 localStorage(双 key: agent_token + portal_token,跨端共享)
- * 2. 跳转到 /workspace
- */
-function handleLoginSuccess(token: string, _employeeId: string, _roles: string[]): void {
- localStorage.setItem('agent_token', token)
- localStorage.setItem('portal_token', token)
- ElMessage.success('登录成功')
- router.push('/workspace')
-}
+/** 表单引用 */
+const formRef = ref
()
-const {
- qrcodePngBase64,
- qrcodeUrl,
- countdown,
- status,
- otpRequired,
- scannedBy,
- loading,
- errorMessage,
- startLogin,
- refreshQrcode,
- stopPolling,
-} = useQrcodeLogin({
- onSuccess: handleLoginSuccess,
- onError: (msg) => ElMessage.error(msg),
+/** 登录表单数据 */
+const loginForm = reactive({
+ userId: '',
+ password: '',
+ otpCode: '',
})
-/**
- * 管理员 OTP 输入按钮(暂未实现完整流程,提示用户去 /itportal/)
- * Phase 2.4 完成后这里跳到 OTP 输入弹窗
- */
-function handleOtpConfirm(): void {
- ElMessage.info('管理员 OTP 二次认证:请前往 /itportal/ 完成(Phase 2.4 即将上线)')
+/** 是否需要 OTP 验证 */
+const requireOtp = ref(false)
+
+/** 登录中状态 */
+const logging = ref(false)
+
+/** 错误信息 */
+const errorMsg = ref('')
+
+/** 表单校验规则 */
+const rules: FormRules = {
+ userId: [
+ { required: true, message: '请输入账号', trigger: 'blur' },
+ ],
+ password: [
+ { required: true, message: '请输入密码', trigger: 'blur' },
+ ],
+ otpCode: [
+ { required: true, message: '请输入 OTP 验证码', trigger: 'blur' },
+ { len: 6, message: '验证码为 6 位数字', trigger: 'blur' },
+ ],
}
// ============================================================================
-// 生命周期
+// 方法
// ============================================================================
-onMounted(() => {
- // 进入页面立即生成二维码
- startLogin()
-})
-onUnmounted(() => {
- // 离开页面停止轮询(防止内存泄漏)
- stopPolling()
-})
+/**
+ * 处理登录
+ */
+async function handleLogin(): Promise {
+ // 表单校验
+ const valid = await formRef.value?.validate().catch(() => false)
+ if (!valid) return
+
+ logging.value = true
+ errorMsg.value = ''
+
+ try {
+ const result = await agentStore.login(
+ loginForm.userId.trim(),
+ loginForm.password,
+ loginForm.otpCode.trim() || undefined
+ )
+
+ // 检查是否需要 OTP 验证
+ if (result && result.require_otp) {
+ requireOtp.value = true
+ loginForm.otpCode = ''
+ ElMessage.info('请输入 OTP 验证码')
+ logging.value = false
+ return
+ }
+
+ // 登录成功
+ ElMessage.success('登录成功')
+ router.push('/workspace')
+ } catch (error: unknown) {
+ const errMsg = error instanceof Error ? error.message : '登录失败,请重试'
+ errorMsg.value = errMsg
+ } finally {
+ logging.value = false
+ }
+}
\ No newline at end of file
+
diff --git a/frontend-h5/dist.zip b/frontend-h5/dist.zip
new file mode 100644
index 0000000..b52361c
Binary files /dev/null and b/frontend-h5/dist.zip differ
diff --git a/frontend-h5/h5-v0.7.5.zip b/frontend-h5/h5-v0.7.5.zip
new file mode 100644
index 0000000..4c5daa9
Binary files /dev/null and b/frontend-h5/h5-v0.7.5.zip differ
diff --git a/frontend-h5/src/api/conversation.ts b/frontend-h5/src/api/conversation.ts
index 6e9d324..b25d18f 100644
--- a/frontend-h5/src/api/conversation.ts
+++ b/frontend-h5/src/api/conversation.ts
@@ -280,16 +280,15 @@ export async function sendMessage(data: SendMessageRequest): Promise {
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
+ try {
+ // 发送上传请求(60 秒超时,大文件上传可能较慢)
+ // 注意:必须显式删除 Content-Type,让浏览器自动生成带 boundary 的 multipart/form-data
+ // 原因:apiClient 实例默认设置了 'Content-Type': 'application/json'
+ // 如果不覆盖,Axios 会保留 application/json,后端无法解析 FormData 中的 file 字段
+ const response: any = await apiClient.post('/upload', formData, {
+ headers: {
+ 'Content-Type': undefined,
+ },
+ timeout: 60000,
+ })
+ // 响应拦截器已确保 code === 0
+ // response = {code:0, data: {url:"...",...}, message:"success"}(拦截器返回值)
+ // response.data = 业务数据 {url:"...", filename:"...", ...}
+ return response.data as UploadResponse
+ } catch (err) {
+ if (attempt === maxRetries) throw err
+ // 指数退避:1s, 2s, 4s
+ const delay = 1000 * Math.pow(2, attempt)
+ console.warn(`[Upload H5] 上传失败,${delay / 1000}秒后重试(第 ${attempt + 1}/${maxRetries} 次)`)
+ await new Promise(r => setTimeout(r, delay))
+ }
+ }
+ // 不应到达此处,但 TypeScript 需要返回值
+ throw new Error('上传失败:已达最大重试次数')
}
diff --git a/frontend-h5/src/components/chat/ChatPanel.vue b/frontend-h5/src/components/chat/ChatPanel.vue
index dbcef9e..612674e 100644
--- a/frontend-h5/src/components/chat/ChatPanel.vue
+++ b/frontend-h5/src/components/chat/ChatPanel.vue
@@ -118,6 +118,7 @@
:trigger-text="store.approvalCardTriggerText"
@select="handleApprovalSelect"
/>
+
diff --git a/frontend-h5/src/components/chat/InputBar.vue b/frontend-h5/src/components/chat/InputBar.vue
index 373c4ee..6a582ec 100644
--- a/frontend-h5/src/components/chat/InputBar.vue
+++ b/frontend-h5/src/components/chat/InputBar.vue
@@ -2,7 +2,7 @@
// 企微IT智能服务台 — H5用户端输入栏组件
// =============================================================================
// 说明:底部输入栏,固定在消息列表下方,包含:
-// [工具栏:表情/图片/文件/拍照] [文本输入框] [发送按钮]
+// [工具栏:表情/文件] [文本输入框] [发送按钮]
// - 输入框默认3行可见,高度随内容动态适应
// - 输入框顶部拖拽手柄可手动调节高度
// - Enter 发送,Shift+Enter 换行
@@ -23,23 +23,14 @@
-
+
-
-
-
-
-
-
@@ -129,15 +102,13 @@
* 布局:[工具栏] [输入框] [发送按钮]
* 输入框固定底部,默认3行可见,高度动态适应
* 顶部拖拽手柄可手动调节输入栏整体高度
- * 工具栏:表情/图片/文件/拍照
- * 支持粘贴图片上传(Ctrl+V)和文件选择上传
+ * 工具栏:表情/文件 (已移除图片/拍照/截图)
+ * 支持文件选择上传(图片上传已禁用)
*/
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { showToast } from 'vant'
-import html2canvas from 'html2canvas-pro'
import { useConversationStore } from '@/stores/conversation'
import { uploadFile } from '@/api/upload'
-import ScreenshotEditor from './ScreenshotEditor.vue'
// ============================================================================
// 工具函数:安全提取错误详情(防止 [object Object])
@@ -176,9 +147,6 @@ const inputFieldRef = ref