refactor: remove duplicate miniapp page sources
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
// pages/activity-mutual-result/activity-mutual-result.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/activity-mutual-waiting/activity-mutual-waiting.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/counseling-intro/counseling-intro.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/counseling-request/counseling-request.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/local-eligibility-supplement/local-eligibility-supplement.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -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<string, string>) {
|
||||
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<EligibilityStatusResponse>('/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' })
|
||||
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 })
|
||||
}
|
||||
},
|
||||
|
||||
uploadSingleFile(filePath: string): Promise<UploadedFile> {
|
||||
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 (this.data.uploadedFileIds.length === 0) {
|
||||
wx.showToast({ title: '请上传佐证材料', icon: 'none' })
|
||||
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
|
||||
}
|
||||
// TODO: P04-V11 调用API提交
|
||||
wx.showLoading({ title: '提交中...' })
|
||||
setTimeout(() => {
|
||||
wx.hideLoading()
|
||||
this.setData({ submitting: true })
|
||||
try {
|
||||
await this.submitEligibility()
|
||||
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 })
|
||||
setTimeout(() => wx.navigateBack(), 800)
|
||||
} catch (error) {
|
||||
wx.showToast({ title: errorMessage(error, '提交失败,请重试'), icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ submitting: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/local-eligibility/local-eligibility.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -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<EligibilityStatus, string>,
|
||||
},
|
||||
|
||||
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
|
||||
// 跳转到补充核验页(如果需要)或直接提交
|
||||
wx.navigateTo({
|
||||
url: `/pages/local-eligibility-supplement/local-eligibility-supplement?type=${type}`
|
||||
async checkStatus() {
|
||||
this.setData({ loading: true, loadError: false })
|
||||
try {
|
||||
const result = await api.get<EligibilityStatusResponse>('/local-eligibility')
|
||||
this.setData({
|
||||
loading: false,
|
||||
status: result.verificationStatus || 'not_declared',
|
||||
eligibilityType: result.eligibilityType || '',
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到已有资格页面
|
||||
go到我的资格() {
|
||||
wx.showToast({ title: '功能开发中', icon: 'none' })
|
||||
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}`,
|
||||
})
|
||||
},
|
||||
|
||||
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' })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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' });
|
||||
},
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/my-counseling/my-counseling.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/premarital-intro/premarital-intro.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/premarital-request/premarital-request.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user