diff --git a/miniapp/pages/activity-mutual-result/activity-mutual-result.js b/miniapp/pages/activity-mutual-result/activity-mutual-result.js deleted file mode 100644 index ffe58c9..0000000 --- a/miniapp/pages/activity-mutual-result/activity-mutual-result.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/activity-mutual-result/activity-mutual-result.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/activity-mutual-waiting/activity-mutual-waiting.js b/miniapp/pages/activity-mutual-waiting/activity-mutual-waiting.js deleted file mode 100644 index aa7adb1..0000000 --- a/miniapp/pages/activity-mutual-waiting/activity-mutual-waiting.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/activity-mutual-waiting/activity-mutual-waiting.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/chat/chat.js b/miniapp/pages/chat/chat.js deleted file mode 100644 index c79fd59..0000000 --- a/miniapp/pages/chat/chat.js +++ /dev/null @@ -1,280 +0,0 @@ -/** - * 聊天详情页 U24(重构:用 messageStore + message-bubble) - * - * 不变量(AGENTS.md): - * - 第 3 条:Mutual Match 后才能聊天——sendMessage 若后端返回 MATCH_REQUIRED, - * UI 给出引导文案;不在客户端做硬阻断,由后端守门。 - * - 第 4 条:Block/Unmatch 后会话 canSendMessage=false,输入栏禁用并显示"已停用"。 - * - 第 6 条:单向活动意向不泄露——本页只展示已建立的 Mutual 会话消息。 - * - 第 7 条:普通工作人员不能查看私人聊天——本页仅服务用户端。 - * - 第 9 条:PostgreSQL 是 Source of Truth——本地仅缓存最近活跃会话消息,冷启动从服务端拉取。 - */ -const { messageStore, appStore, blockUser } = require('../../stores'); -const { messageService } = require('../../services'); -const { to, ROUTE, DANGER_ACTION, BUSINESS_ERROR_CODE } = require('../../constants'); - -/** 超过该间隔则显示时间分隔(5 分钟) */ -const TIME_GAP_MS = 5 * 60 * 1000; - -Page({ - data: { - conversationId: '', - otherDisplayName: '', - otherUserId: '', - otherAvatarUrl: '', - canSend: true, - messages: [], - hasMore: true, - loading: false, - loadingMore: false, - sending: false, - inputText: '', - scrollIntoView: '', - statusBarHeight: 20, - navBarHeight: 44, - showDangerDialog: false, - dangerAction: DANGER_ACTION.BLOCK, - connectionState: 'disconnected', - }, - - onLoad(options) { - const conversationId = options.id || ''; - if (!conversationId) { - appStore.toast('会话不存在'); - wx.navigateBack(); - return; - } - - // 注入状态栏高度供 ui-nav-bar 使用 - try { - const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync(); - this.setData({ - statusBarHeight: info.statusBarHeight || 20, - navBarHeight: info.platform === 'android' ? 48 : 44, - }); - } catch (e) { /* 使用默认值 */ } - - this.setData({ conversationId }); - - // 绑定 messageStore.connectionState 到页面 - this._unsubConn = messageStore.bindToPage(this, { - connectionState: 'connectionState', - }); - // 订阅消息列表 / 分页 / loading 变化 - this._unsubList = messageStore.subscribe((state) => this._syncFromStore(state)); - // 注册实时消息监听:收到对方消息立即标记已读(会话已处于前台) - this._unsubRealtime = messageStore.onMessage(conversationId, (msg) => { - if (msg.bubbleType === 'other') { - messageStore.markConversationRead(conversationId).catch(() => {}); - } - }); - - // 打开会话:拉取最近消息 + 清未读(AGENTS 第 9 条,DB 为准) - messageStore.openConversation(conversationId).catch((err) => appStore.toastError(err)); - // 拉取会话元数据(displayName / canSend / targetUserId / 头像) - this.loadConversationMeta(); - }, - - onUnload() { - this._unsubConn && this._unsubConn(); - this._unsubList && this._unsubList(); - this._unsubRealtime && this._unsubRealtime(); - if (this.data.conversationId) { - // 关闭会话:activeConversationId 置空,后续收到对方消息将累计未读 - messageStore.closeConversation(); - } - }, - - /** 拉取会话元数据(canSend 以服务端为准) */ - async loadConversationMeta() { - try { - const conv = await messageService.getConversation(this.data.conversationId); - if (conv) { - this.setData({ - otherDisplayName: conv.targetDisplayName || conv.otherDisplayName || '用户', - otherUserId: conv.targetUserId || '', - otherAvatarUrl: conv.targetAvatarUrl || '', - canSend: conv.canSendMessage !== false, - }); - } - } catch (err) { - // 兜底:从 store 会话列表查找 - const local = messageStore.state.conversations.find( - (c) => c.conversationId === this.data.conversationId, - ); - if (local) { - this.setData({ - otherDisplayName: local.targetDisplayName || '用户', - otherUserId: local.targetUserId || '', - otherAvatarUrl: local.targetAvatarUrl || '', - canSend: local.canSendMessage !== false, - }); - } else { - appStore.toastError(err); - } - } - }, - - /** 把 store 状态同步到页面 */ - _syncFromStore(state) { - const cid = this.data.conversationId; - const rawList = state.messagesByConversation[cid] || []; - const hasMore = state.hasMoreByConversation[cid] !== false; - const loading = state.conversationsLoading; - const messages = this._formatMessageList(rawList); - const patch = { messages, hasMore }; - if (loading !== this.data.loading) patch.loading = loading; - this.setData(patch); - - // 加载更早消息时,保持滚动位置(定位到加载前的第一条) - if (this._loadingMore && this._preserveMsgId) { - const idx = messages.findIndex((m) => m.messageId === this._preserveMsgId); - if (idx >= 0) this.setData({ scrollIntoView: `msg-${idx}` }); - this._loadingMore = false; - this._preserveMsgId = null; - } else { - this._scrollToLast(messages); - } - }, - - /** 派生时间分隔 / 头像 / 是否显示头像 */ - _formatMessageList(list) { - const otherAvatar = this.data.otherAvatarUrl; - const result = []; - let lastTs = 0; - for (let i = 0; i < list.length; i++) { - const m = list[i]; - const ts = m.sentAt || 0; - const showTime = i === 0 || (ts && ts - lastTs > TIME_GAP_MS); - result.push({ - ...m, - showTime, - sentAtText: showTime ? this._formatTime(ts) : '', - showAvatar: m.bubbleType === 'other', - avatarUrl: m.bubbleType === 'other' ? otherAvatar : '', - }); - lastTs = ts; - } - return result; - }, - - _formatTime(ts) { - if (!ts) return ''; - const d = new Date(ts); - const now = new Date(); - const hm = d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); - if (d.toDateString() === now.toDateString()) return hm; - return `${d.getMonth() + 1}/${d.getDate()} ${hm}`; - }, - - _scrollToLast(list) { - if (!list.length) return; - const id = `msg-${list.length - 1}`; - if (id !== this.data.scrollIntoView) { - this.setData({ scrollIntoView: id }); - } - }, - - onBack() { - const pages = getCurrentPages(); - if (pages.length > 1) { - wx.navigateBack(); - } else { - wx.switchTab({ url: ROUTE.TAB.MESSAGES }); - } - }, - - onInput(e) { - this.setData({ inputText: e.detail.value }); - }, - - async onSend() { - const { inputText, conversationId, sending, canSend } = this.data; - if (!canSend || !inputText.trim() || sending) return; - this.setData({ sending: true, inputText: '' }); - try { - await messageStore.sendMessage(conversationId, inputText.trim()); - } catch (err) { - // MATCH_REQUIRED:双方互相喜欢后才能聊天(AGENTS 第 3 条,后端守门) - if (err && err.code === BUSINESS_ERROR_CODE.MATCH_REQUIRED) { - appStore.toast('双方互相喜欢后才能开始聊天'); - } else { - appStore.toastError(err, '发送失败'); - } - } finally { - this.setData({ sending: false }); - } - }, - - /** 重发失败消息 */ - onRetrySend(e) { - const messageId = e.currentTarget.dataset.msgId; - if (!messageId) return; - messageStore - .retrySend(this.data.conversationId, messageId) - .catch((err) => appStore.toastError(err, '重发失败')); - }, - - /** 滚动到顶部:加载更早消息(保持滚动位置) */ - async onScrollToUpper() { - const { conversationId, hasMore, loadingMore } = this.data; - if (!hasMore || loadingMore) return; - const first = this.data.messages[0]; - this._loadingMore = true; - this._preserveMsgId = first ? first.messageId : null; - this.setData({ loadingMore: true }); - try { - await messageStore.loadMoreMessages(conversationId); - } catch (err) { - appStore.toastError(err); - this._loadingMore = false; - this._preserveMsgId = null; - } finally { - this.setData({ loadingMore: false }); - } - }, - - /** 右上角更多:查看资料 / 举报 / 拉黑 */ - onMore() { - const { otherUserId } = this.data; - if (!otherUserId) { - appStore.toast('暂无更多信息'); - return; - } - wx.showActionSheet({ - itemList: ['查看 Ta 的资料', '举报 Ta', '拉黑 Ta'], - success: (res) => { - if (res.tapIndex === 0) { - wx.navigateTo({ url: to.detail(otherUserId) }); - } else if (res.tapIndex === 1) { - wx.navigateTo({ url: to.report(otherUserId) }); - } else if (res.tapIndex === 2) { - this.setData({ showDangerDialog: true, dangerAction: DANGER_ACTION.BLOCK }); - } - }, - }); - }, - - /** 危险动作确认(拉黑) */ - async onConfirmDanger(e) { - const action = e.detail.action; - const { otherUserId, conversationId } = this.data; - this.setData({ showDangerDialog: false }); - if (action !== DANGER_ACTION.BLOCK) return; - try { - appStore.showLoading('处理中…'); - // 拉黑:触发 MatchStore + MessageStore 同步副作用(AGENTS 第 4 条即时停用) - await blockUser(otherUserId, '用户主动拉黑'); - appStore.hideLoading(); - this.setData({ canSend: false }); - appStore.toastSuccess('已拉黑'); - } catch (err) { - appStore.hideLoading(); - appStore.toastError(err); - } - }, - - onCancelDanger() { - this.setData({ showDangerDialog: false }); - }, -}); diff --git a/miniapp/pages/counseling-intro/counseling-intro.js b/miniapp/pages/counseling-intro/counseling-intro.js deleted file mode 100644 index 7707b4b..0000000 --- a/miniapp/pages/counseling-intro/counseling-intro.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/counseling-intro/counseling-intro.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/counseling-request/counseling-request.js b/miniapp/pages/counseling-request/counseling-request.js deleted file mode 100644 index c5f89f8..0000000 --- a/miniapp/pages/counseling-request/counseling-request.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/counseling-request/counseling-request.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.js b/miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.js deleted file mode 100644 index d26b2b9..0000000 --- a/miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/local-eligibility-supplement/local-eligibility-supplement.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.ts b/miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.ts index c1091ca..511a09e 100644 --- a/miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.ts +++ b/miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.ts @@ -1,76 +1,156 @@ -// 本地资格补充核验页 U63 -// 用户上传佐证材料(工作证明/居住证明等) +import api from '../../utils/api' + +interface EligibilityEvidence { + fileId?: string +} + +interface EligibilityStatusResponse { + eligibilityType?: string + evidences?: EligibilityEvidence[] +} + +interface UploadedFile { + fileKey: string +} + +const errorMessage = (error: unknown, fallback: string) => + error instanceof Error ? error.message : (error as { message?: string })?.message || fallback Page({ data: { eligibilityType: '', - evidenceTypes: [ - { value: 'employment_proof', label: '工作证明', desc: '劳动合同/工牌/在职证明等' }, - { value: 'residence_proof', label: '居住证明', desc: '房产证/租赁合同/水电账单等' }, - { value: 'household_proof', label: '户籍证明', desc: '户口本/户籍页等' }, - { value: 'other', label: '其他材料', desc: '能证明您在丹阳的材料' } - ], selectedType: '', - uploadedFileIds: [] as string[] + uploadedFileIds: [] as string[], + uploading: false, + submitting: false, + maxFiles: 5, + evidenceTypes: [ + { value: 'employment_proof', label: '工作证明', desc: '劳动合同、工牌或在职证明等' }, + { value: 'residence_proof', label: '居住证明', desc: '房产证、租赁合同或水电账单等' }, + { value: 'household_proof', label: '户籍证明', desc: '户口本或户籍页等' }, + { value: 'other', label: '其他材料', desc: '能够说明您在丹阳生活或工作的材料' }, + ], }, onLoad(options: Record) { - if (options.type) { - this.setData({ eligibilityType: options.type }) - } + this.setData({ eligibilityType: options.type || '' }) }, onShow() { - // TODO: P04-V11 加载当前用户的已上传佐证 + this.loadStatus() }, - // 选择佐证类型 - onEvidenceTypeSelect(e: WechatMiniprogram.TouchEvent) { - this.setData({ selectedType: e.currentTarget.dataset.type }) + async loadStatus() { + try { + const result = await api.get('/local-eligibility') + this.setData({ + eligibilityType: this.data.eligibilityType || result.eligibilityType || '', + uploadedFileIds: (result.evidences || []).map((item) => item.fileId).filter(Boolean), + }) + } catch { + // 页面仍允许用户重新选择并提交,错误会在具体操作时反馈。 + } + }, + + onEvidenceTypeSelect(event: WechatMiniprogram.TouchEvent) { + this.setData({ selectedType: event.currentTarget.dataset.type as string }) }, - // 上传图片 onChooseImage() { + if (!this.data.selectedType) { + wx.showToast({ title: '请先选择佐证类型', icon: 'none' }) + return + } + const remaining = this.data.maxFiles - this.data.uploadedFileIds.length + if (remaining <= 0) { + wx.showToast({ title: `最多上传${this.data.maxFiles}份材料`, icon: 'none' }) + return + } wx.chooseMedia({ - count: 6, + count: remaining, mediaType: ['image'], sourceType: ['album', 'camera'], - success: (res) => { - // TODO: P04-V11 上传到对象存储,获取fileId - res.tempFiles.forEach(file => { - console.log('选择的图片:', file.tempFilePath) - }) - wx.showToast({ title: '上传成功', icon: 'success' }) - } + success: ({ tempFiles }) => this.uploadFiles(tempFiles || []), }) }, - // 提交审核 - onSubmit() { - if (!this.data.selectedType) { - wx.showToast({ title: '请选择佐证类型', icon: 'none' }) - return + async uploadFiles(files: WechatMiniprogram.ChooseMediaSuccessCallbackResult['tempFiles']) { + this.setData({ uploading: true }) + try { + for (const file of files) { + const uploaded = await this.uploadSingleFile(file.tempFilePath) + await api.post('/local-eligibility/evidences', { + evidenceType: this.data.selectedType, + fileId: uploaded.fileKey, + }) + this.setData({ uploadedFileIds: [...this.data.uploadedFileIds, uploaded.fileKey] }) + } + wx.showToast({ title: '材料已上传', icon: 'success' }) + } catch (error) { + wx.showToast({ title: errorMessage(error, '上传失败,请重试'), icon: 'none' }) + } finally { + this.setData({ uploading: false }) } - if (this.data.uploadedFileIds.length === 0) { - wx.showToast({ title: '请上传佐证材料', icon: 'none' }) - return - } - // TODO: P04-V11 调用API提交 - wx.showLoading({ title: '提交中...' }) - setTimeout(() => { - wx.hideLoading() - wx.showToast({ title: '已提交审核', icon: 'success' }) - setTimeout(() => { - wx.navigateBack() - }, 1500) - }, 1000) }, - // 删除已上传图片 - onRemoveImage(e: WechatMiniprogram.TouchEvent) { - const index = e.currentTarget.dataset.index - const ids = this.data.uploadedFileIds - ids.splice(index, 1) - this.setData({ uploadedFileIds: ids }) - } + uploadSingleFile(filePath: string): Promise { + return new Promise((resolve, reject) => { + wx.uploadFile({ + url: `${api.baseURL}/photos/upload`, + filePath, + name: 'file', + formData: { photoType: 'local_eligibility', sortOrder: '0' }, + header: { Authorization: `Bearer ${api.getToken() || ''}` }, + success(result) { + let body: { success?: boolean; message?: string; data?: UploadedFile } + try { + body = JSON.parse(result.data || '{}') + } catch { + reject(new Error('上传响应格式错误')) + return + } + if (result.statusCode === 200 && body.success && body.data?.fileKey) { + resolve(body.data) + } else { + reject(new Error(body.message || '上传失败')) + } + }, + fail() { + reject(new Error('网络连接失败')) + }, + }) + }) + }, + + onRemoveImage(event: WechatMiniprogram.TouchEvent) { + const index = Number(event.currentTarget.dataset.index) + this.setData({ + uploadedFileIds: this.data.uploadedFileIds.filter((_, itemIndex) => itemIndex !== index), + }) + }, + + async submitEligibility() { + await api.post('/local-eligibility/declaration', { + eligibilityType: this.data.eligibilityType, + regionCode: '321181', + }) + return api.post('/local-eligibility/submit') + }, + + async onSubmit() { + if (!this.data.eligibilityType) { + wx.showToast({ title: '请选择本地资格类型', icon: 'none' }) + return + } + this.setData({ submitting: true }) + try { + await this.submitEligibility() + wx.showToast({ title: '已提交审核', icon: 'success' }) + setTimeout(() => wx.navigateBack(), 800) + } catch (error) { + wx.showToast({ title: errorMessage(error, '提交失败,请重试'), icon: 'none' }) + } finally { + this.setData({ submitting: false }) + } + }, }) diff --git a/miniapp/pages/local-eligibility/local-eligibility.js b/miniapp/pages/local-eligibility/local-eligibility.js deleted file mode 100644 index 65bc1eb..0000000 --- a/miniapp/pages/local-eligibility/local-eligibility.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/local-eligibility/local-eligibility.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/local-eligibility/local-eligibility.ts b/miniapp/pages/local-eligibility/local-eligibility.ts index 87051c1..f74b92c 100644 --- a/miniapp/pages/local-eligibility/local-eligibility.ts +++ b/miniapp/pages/local-eligibility/local-eligibility.ts @@ -1,42 +1,84 @@ -// 本地资格说明页 U62 -// 解释为什么需要本地资格,引导用户声明 +import api from '../../utils/api' + +type EligibilityStatus = + | 'not_declared' + | 'declared' + | 'needs_evidence' + | 'pending_review' + | 'verified' + | 'rejected' + +interface EligibilityStatusResponse { + verificationStatus?: EligibilityStatus + eligibilityType?: string +} Page({ data: { + loading: true, + loadError: false, + submitting: false, + status: 'not_declared' as EligibilityStatus, + eligibilityType: '', eligibilityTypes: [ - { value: 'household_registration', label: '丹阳户籍', icon: '🏠' }, - { value: 'employment', label: '在丹阳工作', icon: '💼' }, - { value: 'residence', label: '在丹阳长期居住', icon: '🏡' }, - { value: 'other', label: '其他符合服务条件', icon: '⭐' } - ] + { value: 'household_registration', label: '丹阳户籍', icon: '户' }, + { value: 'employment', label: '在丹阳工作', icon: '工' }, + { value: 'residence', label: '在丹阳长期居住', icon: '居' }, + { value: 'other', label: '其他符合服务条件', icon: '其' }, + ], + statusText: { + not_declared: '尚未声明', + declared: '声明已保存', + needs_evidence: '需要补充佐证', + pending_review: '审核中', + verified: '已核验', + rejected: '未通过', + } as Record, }, onLoad() { - // 检查当前用户的本地资格状态 this.checkStatus() }, onShow() { - this.checkStatus() + if (!this.data.loading) this.checkStatus() }, - checkStatus() { - const app = getApp<{ getStorageSync: (key: string) => any }>() - // TODO: P04-V11 接入真实API - // 如果已声明并verified,跳转到已完成提示 + onPullDownRefresh() { + this.checkStatus().finally(() => wx.stopPullDownRefresh()) }, - // 选择资格类型 - onTypeSelect(e: WechatMiniprogram.TouchEvent) { - const type = e.currentTarget.dataset.type as string - // 跳转到补充核验页(如果需要)或直接提交 + async checkStatus() { + this.setData({ loading: true, loadError: false }) + try { + const result = await api.get('/local-eligibility') + this.setData({ + loading: false, + status: result.verificationStatus || 'not_declared', + eligibilityType: result.eligibilityType || '', + }) + return result + } catch { + this.setData({ loading: false, loadError: true }) + return null + } + }, + + onTypeSelect(event: WechatMiniprogram.TouchEvent) { + const type = event.currentTarget.dataset.type as string wx.navigateTo({ - url: `/pages/local-eligibility-supplement/local-eligibility-supplement?type=${type}` + url: `/pages/local-eligibility-supplement/local-eligibility-supplement?type=${type}`, }) }, - // 跳转到已有资格页面 - go到我的资格() { - wx.showToast({ title: '功能开发中', icon: 'none' }) - } + goToMyEligibility() { + const { status, eligibilityType, statusText } = this.data + if (status === 'needs_evidence' || status === 'rejected' || status === 'declared') { + wx.navigateTo({ + url: `/pages/local-eligibility-supplement/local-eligibility-supplement?type=${eligibilityType || ''}`, + }) + return + } + wx.showToast({ title: statusText[status] || '状态已更新', icon: 'none' }) + }, }) diff --git a/miniapp/pages/login/login.js b/miniapp/pages/login/login.js deleted file mode 100644 index 8b86866..0000000 --- a/miniapp/pages/login/login.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * 登录页 — 微信一键登录(规范 §4.1) - * - * 流程: - * 1. wx.login() 获取临时 code - * 2. POST /api/v1/auth/wechat/login 换取 JWT - * 3. 存储 token → authStore.isLoggedIn = true - * 4. 返回上一页(或由路由系统自动跳转首页) - */ -const { authStore, appStore } = require('../../stores'); -const { ROUTE } = require('../../constants'); - -Page({ - data: { - loggingIn: false, - error: '', - platformName: getApp().globalData.platformName, - slogan: getApp().globalData.slogan, - }, - - onLoad() { - // 如果已登录,直接返回 - if (authStore.state.isLoggedIn) { - const pages = getCurrentPages(); - if (pages.length > 1) { - wx.navigateBack(); - } else { - wx.switchTab({ url: ROUTE.TAB.HOME }); - } - } - }, - - async onLoginTap() { - if (this.data.loggingIn) return; - this.setData({ loggingIn: true, error: '' }); - - try { - // 1. 获取微信登录 code - const loginRes = await new Promise((resolve, reject) => { - wx.login({ - success: resolve, - fail: reject, - }); - }); - - const code = loginRes.code; - if (!code) { - throw new Error('获取微信登录凭证失败'); - } - - // 2. 调用后端微信登录接口 - const result = await authStore.loginWithWechat(code); - - // 3. 登录成功 → 返回上一页或跳转首页 - const pages = getCurrentPages(); - if (pages.length > 1) { - wx.navigateBack(); - } else { - wx.switchTab({ url: ROUTE.TAB.HOME }); - } - } catch (err) { - console.error('[Login] 登录失败:', err); - this.setData({ - error: err?.message || '登录失败,请重试', - loggingIn: false, - }); - appStore.toastError(err?.message || '登录失败,请重试'); - } - }, - - onViewPrivacy() { - wx.navigateTo({ url: '/pages/help-center/help-center?tab=privacy' }); - }, - - onViewTerms() { - wx.navigateTo({ url: '/pages/help-center/help-center?tab=terms' }); - }, -}); diff --git a/miniapp/pages/my-counseling/my-counseling.js b/miniapp/pages/my-counseling/my-counseling.js deleted file mode 100644 index fc99f06..0000000 --- a/miniapp/pages/my-counseling/my-counseling.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/my-counseling/my-counseling.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/premarital-intro/premarital-intro.js b/miniapp/pages/premarital-intro/premarital-intro.js deleted file mode 100644 index 383da7a..0000000 --- a/miniapp/pages/premarital-intro/premarital-intro.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/premarital-intro/premarital-intro.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/miniapp/pages/premarital-request/premarital-request.js b/miniapp/pages/premarital-request/premarital-request.js deleted file mode 100644 index 8549d64..0000000 --- a/miniapp/pages/premarital-request/premarital-request.js +++ /dev/null @@ -1,66 +0,0 @@ -// pages/premarital-request/premarital-request.js -Page({ - - /** - * 页面的初始数据 - */ - data: { - - }, - - /** - * 生命周期函数--监听页面加载 - */ - onLoad(options) { - - }, - - /** - * 生命周期函数--监听页面初次渲染完成 - */ - onReady() { - - }, - - /** - * 生命周期函数--监听页面显示 - */ - onShow() { - - }, - - /** - * 生命周期函数--监听页面隐藏 - */ - onHide() { - - }, - - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - - }, - - /** - * 页面相关事件处理函数--监听用户下拉动作 - */ - onPullDownRefresh() { - - }, - - /** - * 页面上拉触底事件的处理函数 - */ - onReachBottom() { - - }, - - /** - * 用户点击右上角分享 - */ - onShareAppMessage() { - - } -}) \ No newline at end of file diff --git a/ui-tests/miniapp-source-uniqueness-contracts.mjs b/ui-tests/miniapp-source-uniqueness-contracts.mjs new file mode 100644 index 0000000..9b84c83 --- /dev/null +++ b/ui-tests/miniapp-source-uniqueness-contracts.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict' +import { existsSync, readdirSync } from 'node:fs' +import { extname, join, relative, resolve } from 'node:path' + +const root = resolve(import.meta.dirname, '..') +const pagesRoot = resolve(root, 'miniapp/pages') + +function walk(directory) { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const absolutePath = join(directory, entry.name) + return entry.isDirectory() ? walk(absolutePath) : [absolutePath] + }) +} + +const duplicatePageSources = walk(pagesRoot) + .filter((file) => extname(file) === '.ts') + .filter((file) => existsSync(file.replace(/\.ts$/, '.js'))) + .map((file) => relative(root, file).replace(/\\/g, '/')) + +assert.deepEqual( + duplicatePageSources, + [], + `小程序页面存在 TS/JS 双源,可能导致微信开发者工具加载旧实现:\n${duplicatePageSources.join('\n')}`, +) + +console.log('Miniapp page source uniqueness contracts passed') diff --git a/ui-tests/phase04-local-eligibility-contracts.mjs b/ui-tests/phase04-local-eligibility-contracts.mjs new file mode 100644 index 0000000..788e908 --- /dev/null +++ b/ui-tests/phase04-local-eligibility-contracts.mjs @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import vm from 'node:vm' +import ts from '../miniapp/node_modules/typescript/lib/typescript.js' + +function loadPage(relativePath, api) { + let page + const source = readFileSync(new URL(`../${relativePath}`, import.meta.url), 'utf8') + const executableSource = relativePath.endsWith('.ts') + ? ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText + : source + vm.runInNewContext(executableSource, { + exports: {}, + module: { exports: {} }, + require(request) { + if (request === '../../utils/api') return { default: api } + throw new Error(`未预期依赖:${request}`) + }, + Page(config) { page = config }, + wx: {}, + console, + }, { filename: relativePath }) + return page +} + +const statusCalls = [] +const declarationPage = loadPage('miniapp/pages/local-eligibility/local-eligibility.ts', { + get: async (url) => { + statusCalls.push(url) + return { verificationStatus: 'needs_evidence', eligibilityType: 'employment' } + }, +}) +assert.equal(typeof declarationPage.checkStatus, 'function', 'U62 必须实现资格状态查询') +await declarationPage.checkStatus.call({ + setData(data) { this.data = { ...(this.data || {}), ...data } }, + data: {}, +}) +assert.deepEqual(statusCalls, ['/local-eligibility'], 'U62 必须从当前用户资格接口读取状态') + +const submitCalls = [] +const supplementPage = loadPage('miniapp/pages/local-eligibility-supplement/local-eligibility-supplement.ts', { + post: async (url, data) => { + submitCalls.push([url, data]) + return { verificationStatus: 'pending_review' } + }, +}) +assert.equal(typeof supplementPage.submitEligibility, 'function', 'U63 必须实现声明与提交审核链路') +await supplementPage.submitEligibility.call({ + data: { eligibilityType: 'employment', selectedType: '', uploadedFileIds: [] }, + setData() {}, +}) +assert.deepEqual( + submitCalls.map(([url]) => url), + ['/local-eligibility/declaration', '/local-eligibility/submit'], + '无需补证时必须先声明,再提交审核', +) + +console.log('Phase 04 local eligibility contracts passed')