<template> <view :class="['app-container', theme]...
作成日: 2026年4月20日
使用モデル GPT-5.4 Thinking by Chat01
作成日: 2026年4月20日
使用モデル GPT-5.4 Thinking by Chat01
</template> <script setup> import { ref, inject } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { useUserecruitmentsharingStore } from '@/store/Userecruitmentsharing.js' import { onLoad } from '@dcloudio/uni-app' import ajax from '@/api/ajax.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' const store = useMainStore() const { theme } = storeToRefs(store) const roleStore = useUserecruitmentsharingStore() const { isApplicant, organizationId, applicantId, organizationAvatarUrl, applicantAvatarUrl } = storeToRefs(roleStore) const injectedServerUrl = inject('webProjectUrl', '') || '' const statusBarHeight = ref(Number(getStatusBarHeight?.() || uni.getSystemInfoSync()?.statusBarHeight || 0)) const headerBarHeight = ref(Number(getTitleBarHeight?.() || 44)) const headerTotalPx = ref(Number(statusBarHeight.value || 0) + Number(headerBarHeight.value || 0)) const previewUrl = ref('') const tempFilePath = ref('') const submitting = ref(false) function toast(title) { uni.showToast({ title: String(title || ''), icon: 'none' }) } function navigateBack() { uni.navigateBack() } function safeDecode(v) { let s = String(v ?? '') for (let i = 0; i < 2; i++) { if (!/%[0-9A-Fa-f]{2}/.test(s)) break try { s = decodeURIComponent(s) } catch (e) { break } } return s } function buildFileUrl(path) { if (!path) return '' if (/^https?:\/\//i.test(path)) return path if (!injectedServerUrl) return path return injectedServerUrl.replace(/\/$/, '') + '/' + String(path).replace(/^\//, '') } function normalizeBaseUrl(url) { return String(url || '').replace(/\/$/, '') } function openChooseDialog() { uni.showActionSheet({ itemList: ['拍照', '从手机相册选择'], itemColor: '#1d1d1f', success: (res) => { const sourceType = Number(res.tapIndex) === 0 ? ['camera'] : ['album'] chooseImage(sourceType) } }) } function chooseImage(sourceType = ['album']) { uni.chooseImage({ count: 1, sourceType, sizeType: ['compressed', 'original'], success: (res) => { const path = res.tempFilePaths?.[0] if (path) { tempFilePath.value = path previewUrl.value = path } } }) } function uploadAvatarFile(filePath) { return new Promise((resolve, reject) => { const base = normalizeBaseUrl(injectedServerUrl) const url = isApplicant.value ? 'recrueitJobApplicantUpdateAvatarUr' : 'organizationUpdateAvatarUrl' const formData = {} if (isApplicant.value) { formData.recrueitJobApplicantId = applicantId.value } else { formData.organizationId = organizationId.value } uni.uploadFile({ url: `${base}/${url}`, filePath, name: 'avatarFile', formData, header: ajax?.getHeaders?.() || {}, success: (res) => { console.log(res,'9999999999999999999999999999') try { const json = typeof res.data === 'string' ? JSON.parse(res.data) : res.data if (json?.result === 'success' || json?.avatarUrl || json?.uploadedFileId) { resolve(json) return } reject(json) } catch (e) { reject(e) } }, fail: reject }) }) } function getEventChannelSafely() { try { if (typeof getOpenerEventChannel === 'function') { return getOpenerEventChannel() } return null } catch (e) { return null } } async function submitAvatar() { if (submitting.value) return if (isApplicant.value && !applicantId.value) { toast('缺少求职者档案信息') return } if (!isApplicant.value && !organizationId.value) { toast('缺少机构信息') return } if (!tempFilePath.value) { openChooseDialog() return } submitting.value = true uni.showLoading({ title: '安全上传中...', mask: true }) try { const res = await uploadAvatarFile(tempFilePath.value) uni.hideLoading() const newAvatar = res?.avatarUrl || previewUrl.value if (isApplicant.value) { roleStore.updateApplicantAvatar(newAvatar) } else { roleStore.updateOrganizationAvatar(newAvatar) } const eventChannel = getEventChannelSafely() eventChannel?.emit('avatarUpdated', { avatarUrl: newAvatar }) uni.showToast({ title: '头像已更新', icon: 'success' }) setTimeout(() => { uni.navigateBack() }, 800) } catch (e) { uni.hideLoading() toast('上传失败,请重试') } finally { submitting.value = false } } onLoad((option) => { roleStore.hydrateFromStorage() const currentRawAvatar = safeDecode( option?.avatarUrl || (isApplicant.value ? applicantAvatarUrl.value : organizationAvatarUrl.value) || '' ) previewUrl.value = buildFileUrl(currentRawAvatar) }) </script> <style lang="scss" scoped> .app-container { min-height: 100vh; background: #f7f8fa; } .header { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(247, 248, 250, 0.9); backdrop-filter: blur(10px); } .header-inner { display: flex; align-items: center; justify-content: space-between; height: 100%; padding: 0 30rpx; } .left-box, .right-box { width: 80rpx; display: flex; align-items: center; } .left-box { justify-content: flex-start; } .right-box { justify-content: flex-end; } .title-text { font-size: 34rpx; font-weight: 600; color: #1d1d1f; } .page-main { display: flex; flex-direction: column; align-items: center; min-height: 100vh; box-sizing: border-box; } .avatar-editor { margin-top: 140rpx; display: flex; flex-direction: column; align-items: center; } .avatar-circle { position: relative; width: 300rpx; height: 300rpx; border-radius: 50%; background: #ffffff; box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.08); display: flex; align-items: center; justify-content: center; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .circle-hover { transform: scale(0.94); } .avatar-img { width: 100%; height: 100%; border-radius: 50%; border: 6rpx solid #ffffff; box-sizing: border-box; } .avatar-placeholder { display: flex; align-items: center; justify-content: center; } .camera-badge { position: absolute; right: 10rpx; bottom: 16rpx; width: 72rpx; height: 72rpx; border-radius: 50%; background: #1d1d1f; border: 6rpx solid #ffffff; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2); } .tip-text { margin-top: 50rpx; font-size: 28rpx; color: #86868b; } .bottom-action { margin-top: auto; margin-bottom: 120rpx; width: 85%; } .btn-submit { width: 100%; height: 96rpx; border-radius: 48rpx; background: #1d1d1f; color: #ffffff; font-size: 32rpx; font-weight: 600; display: flex; align-items: center; justify-content: center; box-shadow: 0 12rpx 30rpx rgba(29, 29, 31, 0.2); transition: all 0.2s; } .btn-hover { opacity: 0.85; transform: scale(0.98); } </style>配合这个再结合import { defineStore } from 'pinia'text<view class="page-main" :style="{ paddingTop: headerTotalPx + 'px' }"> <view class="avatar-editor"> <view class="avatar-circle" hover-class="circle-hover" @click="openChooseDialog"> <image v-if="previewUrl" class="avatar-img" :src="previewUrl" mode="aspectFill" /> <view v-else class="avatar-placeholder"> <uni-icons type="person-filled" color="#c0c4cc" size="80" /> </view> <view class="camera-badge"> <uni-icons type="camera-filled" color="#ffffff" size="20" /> </view> </view> <text class="tip-text">点击上方区域更换照片</text> </view> <view class="bottom-action"> <view class="btn-submit" hover-class="btn-hover" @click="submitAvatar"> {{ submitting ? '安全上传中...' : '确认更新' }} </view> </view> </view> </view>
import { ref, computed } from 'vue'
const STORAGE_KEYS = {
identity: 'coffee_identity',
consumerId: 'consumerId',
userId: 'coffee_user_id',
userState: 'coffee_user_state',
organizationId: 'organizationId',
applicantId: 'recrueitJobApplicantId',
organizationAvatarUrl: 'coffee_enterprise_avatar',
applicantAvatarUrl: 'coffee_applicant_avatar',
organizationName: 'coffee_enterprise_name',
applicantName: 'coffee_applicant_name',
enterpriseRegisterForm: 'coffee_enterprise_register_form',
applicantRegisterForm: 'coffee_applicant_register_form',
editingPostDraft: 'coffee_editing_recruit_post',
recruitSessions: 'coffee_recruit_sessions'
}
const RECRUIT_CHAT_STATE_PREFIX = 'coffee_recruit_chat_state:'
function normalizeIdentity(value) {
return String(value || '') === 'applicant' ? 'applicant' : 'enterprise'
}
function safeGetStorage(key, fallback = '') {
try {
const value = uni.getStorageSync(key)
return value === undefined || value === null || value === '' ? fallback : value
} catch (e) {
return fallback
}
}
function safeSetStorage(key, value) {
try {
uni.setStorageSync(key, value)
} catch (e) {}
}
function safeRemoveStorage(key) {
try {
uni.removeStorageSync(key)
} catch (e) {}
}
function safeReadJSON(key, fallback) {
try {
const raw = uni.getStorageSync(key)
if (!raw) return fallback
if (typeof raw === 'object') return raw
return JSON.parse(raw)
} catch (e) {
return fallback
}
}
function safeWriteJSON(key, value) {
try {
uni.setStorageSync(key, JSON.stringify(value ?? {}))
} catch (e) {}
}
function deepClone(value) {
return JSON.parse(JSON.stringify(value ?? null))
}
function mergeObject(base = {}, patch = {}) {
const target = Array.isArray(base) ? [...base] : { ...base }
Object.keys(patch || {}).forEach((key) => {
const oldValue = target[key]
const newValue = patch[key]
if (
oldValue &&
newValue &&
Object.prototype.toString.call(oldValue) === '[object Object]' &&
Object.prototype.toString.call(newValue) === '[object Object]'
) {
target[key] = mergeObject(oldValue, newValue)
} else {
target[key] = newValue
}
})
return target
}
function createEnterpriseRegisterForm() {
return {
password: '',
confirmPassword: '',
organizationFileList: [],
fileIndexList: [],
currentOrganizationTypeId: '',
organization: {
name: '',
phoneticism: '',
shortName: '',
setUpTime: '',
taxpayerIdentityNumber: '',
province: '',
city: '',
area: '',
state: 1,
type: 3,
address: '',
aboutUs: '',
enterpriseScale: '',
companyProfile: '',
isExRegist: '咖招',
industry: '',
latitude: '',
longitude: '',
bank: '',
bankAccount: '',
introduction: '',
organizationAttribute: '企业'
},
personnel: {
mobilePhone: '',
address: '',
name: '',
idCard: '',
email: '',
duty: ''
},
user: {
username: '',
mobilePhone: '',
address: '',
name: '',
idCard: ''
}
}
}
function createApplicantRegisterForm() {
return {
id: '',
name: '',
avatarUrl: '',
education: '',
performerId: '',
position: '',
state: 1,
currentAddress: '',
salaryExpectation: ''
}
}
function normalizeRecruitMessageValue(value) {
return String(value ?? '').trim()
}
function fileExtensionFromValue(value = '') {
const raw = String(value || '').split('?')[0].split('#')[0]
const matched = raw.match(/.([a-zA-Z0-9]+)$/)
return matched ? matched[1].toLowerCase() : ''
}
function inferRecruitMediaKind(message = {}) {
if (message?.mediaKind === 'image' || message?.localMediaKind === 'image') return 'image'
if (message?.mediaKind === 'video' || message?.localMediaKind === 'video') return 'video'
if (Number(message?.type || 0) === 1) return 'image'
if (Number(message?.type || 0) === 2) return 'video'
const sourceList = [message?.playURL, message?.showName, message?.fileName].filter(Boolean)
for (const source of sourceList) {
const ext = fileExtensionFromValue(source)
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif'].includes(ext)) return 'image'
if (['mp4', 'mov', 'm4v', 'webm', 'avi', 'mkv', '3gp'].includes(ext)) return 'video'
}
return ''
}
function extractRecruitTextValue(message = {}) {
const raw = message?.content
if (raw === null || raw === undefined) return ''
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === 'object') {
if (typeof parsed.text === 'string') return parsed.text.trim()
if (typeof parsed.content === 'string') return parsed.content.trim()
if (typeof parsed.value === 'string') return parsed.value.trim()
}
} catch (e) {}
return raw.trim()
}
if (typeof raw === 'object') {
if (typeof raw.text === 'string') return raw.text.trim()
if (typeof raw.content === 'string') return raw.content.trim()
if (typeof raw.value === 'string') return raw.value.trim()
}
return normalizeRecruitMessageValue(raw)
}
function buildRecruitSemanticKey(message = {}) {
const mediaKind = inferRecruitMediaKind(message)
const sender = String(message?.consumerId || '')
const type = String(Number(message?.type || 0))
if (mediaKind) {
const mediaRef = normalizeRecruitMessageValue(message?.playURL || message?.fileName || message?.showName)
return [sender, type, mediaKind, mediaRef].join('|')
}
const text = extractRecruitTextValue(message)
return [sender, type, text].join('|')
}
function buildRecruitMessageToken(message = {}) {
return [
message.id || '',
message.localId || '',
message.createTime || '',
message.consumerId || '',
message.consumerTalk ? '1' : '0',
message.type || '0',
message.content || '',
message.playURL || '',
message.showName || '',
message.fileName || '',
message.mediaKind || message.localMediaKind || ''
].join('|')
}
function canMergeRecruitMessages(current = {}, incoming = {}) {
if (incoming?.id && current?.id && String(incoming.id) === String(current.id)) return true
const currentToken = buildRecruitMessageToken(current)
const incomingToken = buildRecruitMessageToken(incoming)
if (currentToken && incomingToken && currentToken === incomingToken) return true
const currentSemantic = buildRecruitSemanticKey(current)
const incomingSemantic = buildRecruitSemanticKey(incoming)
const currentTime = new Date(current?.createTime || 0).getTime()
const incomingTime = new Date(incoming?.createTime || 0).getTime()
if (currentSemantic && incomingSemantic && currentSemantic === incomingSemantic) {
if (currentTime && incomingTime && Math.abs(currentTime - incomingTime) <= 60 * 1000) return true
if (!currentTime || !incomingTime) return true
}
if (!(incoming?.id && !current?.id)) return false
if (!(current?.localId || current?.sendStatus === 'sending' || current?.sendStatus === 'success')) return false
const currentType = Number(current?.type || 0)
const incomingType = Number(incoming?.type || 0)
const textLike = [0, 6, 9]
if (currentType !== incomingType && !(textLike.includes(currentType) && textLike.includes(incomingType))) return false
if (!currentTime || !incomingTime || Math.abs(currentTime - incomingTime) > 60 * 1000) return false
if (String(current?.consumerId || '') && String(incoming?.consumerId || '') && String(current.consumerId) !== String(incoming.consumerId)) {
return false
}
const currentMediaKind = inferRecruitMediaKind(current)
const incomingMediaKind = inferRecruitMediaKind(incoming)
if (currentMediaKind || incomingMediaKind) {
if (currentMediaKind !== incomingMediaKind) return false
const currentName = normalizeRecruitMessageValue(current?.showName || current?.fileName)
const incomingName = normalizeRecruitMessageValue(incoming?.showName || incoming?.fileName)
if (currentName && incomingName && currentName === incomingName) return true
const currentUrl = normalizeRecruitMessageValue(current?.playURL || current?.fileName)
const incomingUrl = normalizeRecruitMessageValue(incoming?.playURL || incoming?.fileName)
return !!currentUrl && !!incomingUrl && (currentUrl === incomingUrl || fileExtensionFromValue(currentUrl) === fileExtensionFromValue(incomingUrl))
}
const currentContent = extractRecruitTextValue(current)
const incomingContent = extractRecruitTextValue(incoming)
return !!currentContent && currentContent === incomingContent
}
function createRecruitChatState() {
return {
messages: [],
deletedMessageIds: [],
deletedTokens: [],
recalledMessageIds: [],
recalledTokens: [],
clearedBeforeTime: ''
}
}
function uniqueArray(arr = []) {
return Array.from(new Set((arr || []).filter(Boolean)))
}
function sortRecruitMessages(messages = []) {
return [...messages].sort((a, b) => {
return new Date(a.createTime || 0).getTime() - new Date(b.createTime || 0).getTime()
})
}
function normalizeSession(payload = {}) {
return {
sessionKey: String(payload.sessionKey || ''),
role: normalizeIdentity(payload.role || 'enterprise'),
recrueitPostId: String(payload.recrueitPostId || ''),
peerId: String(payload.peerId || payload.peerConsumerId || payload.peerApplicantId || payload.peerOrganizationId || payload.talkConsumerId || ''),
peerConsumerId: String(payload.peerConsumerId || ''),
peerApplicantId: String(payload.peerApplicantId || ''),
peerOrganizationId: String(payload.peerOrganizationId || ''),
peerName: String(payload.peerName || ''),
peerAvatar: String(payload.peerAvatar || ''),
talkConsumerId: String(payload.talkConsumerId || ''),
postTitle: String(payload.postTitle || ''),
postPosition: String(payload.postPosition || ''),
salaryRange: String(payload.salaryRange || ''),
lastMessage: String(payload.lastMessage || ''),
lastMessageType: Number(payload.lastMessageType || 0),
lastMessageTime: String(payload.lastMessageTime || payload.updatedAt || ''),
headerCard: payload.headerCard ? deepClone(payload.headerCard) : null,
unreadCount: Number(payload.unreadCount || 0),
pinned: !!payload.pinned,
updatedAt: String(payload.updatedAt || payload.lastMessageTime || '')
}
}
export const useUserecruitmentsharingStore = defineStore('Userecruitmentsharing', () => {
const identity = ref('enterprise')
const consumerId = ref('')
const userId = ref('')
const userState = ref('')
const organizationId = ref('')
const applicantId = ref('')
const organizationAvatarUrl = ref('')
const applicantAvatarUrl = ref('')
const organizationName = ref('')
const applicantName = ref('')
const enterpriseRegisterForm = ref(createEnterpriseRegisterForm())
const applicantRegisterForm = ref(createApplicantRegisterForm())
const editingPostDraft = ref(null)
const recruitSessions = ref([])
const isEnterprise = computed(() => identity.value === 'enterprise')
const isApplicant = computed(() => identity.value === 'applicant')
const currentAvatarUrl = computed(() => (isEnterprise.value ? organizationAvatarUrl.value : applicantAvatarUrl.value))
const currentDisplayName = computed(() => (isEnterprise.value ? organizationName.value : applicantName.value))
const recrueitJobApplicantId = computed({
get: () => applicantId.value,
set: (val) => {
applicantId.value = String(val || '')
persistBase()
}
})
function makeRecruitChatCacheKey(payload = {}) {
const cid = String(payload.consumerId || consumerId.value || '')
const postId = String(payload.recrueitPostId || '')
const peer = String(
payload.peerConsumerId ||
payload.peerApplicantId ||
payload.peerOrganizationId ||
payload.peerId ||
payload.talkConsumerId ||
''
)
const role = String(payload.role || normalizeIdentity(identity.value))
return [cid, postId, peer, role].join('::')
}
function makeRecruitSessionKey(payload = {}) {
const role = String(payload.role || normalizeIdentity(identity.value))
const postId = String(payload.recrueitPostId || '')
const peer = String(
payload.peerConsumerId ||
payload.peerApplicantId ||
payload.peerOrganizationId ||
payload.peerId ||
payload.talkConsumerId ||
''
)
return [role, postId, peer].join('::')
}
function stateStorageKey(key) {
return RECRUIT_CHAT_STATE_PREFIX + String(key || '')
}
function readRecruitChatState(key) {
return mergeObject(createRecruitChatState(), safeReadJSON(stateStorageKey(key), createRecruitChatState()) || {})
}
function writeRecruitChatState(key, state) {
safeWriteJSON(stateStorageKey(key), mergeObject(createRecruitChatState(), state || {}))
}
function writeRecruitSessions(list = []) {
recruitSessions.value = [...list]
safeWriteJSON(STORAGE_KEYS.recruitSessions, recruitSessions.value)
}
function listRecruitSessions() {
return [...(recruitSessions.value || [])].sort((a, b) => {
const pin = Number(!!b.pinned) - Number(!!a.pinned)
if (pin !== 0) return pin
return new Date(b.updatedAt || b.lastMessageTime || 0).getTime() - new Date(a.updatedAt || a.lastMessageTime || 0).getTime()
})
}
function registerRecruitSession(payload = {}) {
const sessionKey = payload.sessionKey || makeRecruitSessionKey(payload)
const normalized = normalizeSession({
...payload,
sessionKey,
updatedAt: payload.updatedAt || payload.lastMessageTime || new Date().toISOString()
})
const list = [...recruitSessions.value]
const idx = list.findIndex((item) => item.sessionKey === sessionKey)
if (idx >= 0) list[idx] = mergeObject(list[idx], normalized)
else list.unshift(normalized)
writeRecruitSessions(list)
return normalized
}
function touchRecruitSession(payload = {}) {
return registerRecruitSession({
...payload,
lastMessageTime: payload.lastMessageTime || new Date().toISOString(),
updatedAt: payload.updatedAt || payload.lastMessageTime || new Date().toISOString()
})
}
function markRecruitSessionRead(sessionKey = '') {
if (!sessionKey) return
writeRecruitSessions(
recruitSessions.value.map((item) => item.sessionKey === sessionKey ? { ...item, unreadCount: 0 } : item)
)
}
function bumpRecruitSessionUnread(sessionKey = '', extra = 1) {
if (!sessionKey) return
writeRecruitSessions(
recruitSessions.value.map((item) => item.sessionKey === sessionKey ? {
...item,
unreadCount: Number(item.unreadCount || 0) + Number(extra || 1),
updatedAt: new Date().toISOString()
} : item)
)
}
function removeRecruitSession(sessionKey = '') {
if (!sessionKey) return
writeRecruitSessions(recruitSessions.value.filter((item) => item.sessionKey !== sessionKey))
}
function saveRecruitChatCache(key, messages = []) {
const state = readRecruitChatState(key)
state.messages = sortRecruitMessages((messages || []).map((item) => deepClone(item)))
writeRecruitChatState(key, state)
return state.messages
}
function readRecruitChatCache(key) {
return readRecruitChatState(key).messages || []
}
function appendRecruitChatMessage(key, message) {
const state = readRecruitChatState(key)
const token = buildRecruitMessageToken(message)
const semanticKey = buildRecruitSemanticKey(message)
const idx = state.messages.findIndex(
(item) => (message.id && item.id && item.id === message.id) || buildRecruitMessageToken(item) === token || (semanticKey && buildRecruitSemanticKey(item) === semanticKey) || canMergeRecruitMessages(item, message)
)
if (idx >= 0) state.messages[idx] = mergeObject(state.messages[idx], deepClone(message))
else state.messages.push(deepClone(message))
state.messages = sortRecruitMessages(state.messages)
writeRecruitChatState(key, state)
return state.messages
}
function deleteRecruitChatMessage(key, message) {
const state = readRecruitChatState(key)
const token = buildRecruitMessageToken(message)
if (message?.id) state.deletedMessageIds = uniqueArray([...state.deletedMessageIds, String(message.id)])
state.deletedTokens = uniqueArray([...state.deletedTokens, token])
state.messages = state.messages.filter((item) => {
if (message?.id && item?.id && String(item.id) === String(message.id)) return false
return buildRecruitMessageToken(item) !== token
})
writeRecruitChatState(key, state)
return state.messages
}
function recallRecruitChatMessage(key, message, recalledText = '你撤回了一条消息') {
const state = readRecruitChatState(key)
const token = buildRecruitMessageToken(message)
if (message?.id) state.recalledMessageIds = uniqueArray([...state.recalledMessageIds, String(message.id)])
state.recalledTokens = uniqueArray([...state.recalledTokens, token])
state.messages = state.messages.map((item) => {
const matched = (message?.id && item?.id && String(item.id) === String(message.id)) || buildRecruitMessageToken(item) === token
if (!matched) return item
return {
...item,
recalled: true,
recalledText,
content: '',
type: 0,
showName: '',
playURL: '',
fileName: ''
}
})
writeRecruitChatState(key, state)
return state.messages
}
function applyRecruitStateToMessages(key, messages = []) {
const state = readRecruitChatState(key)
const clearedAt = state.clearedBeforeTime ? new Date(state.clearedBeforeTime).getTime() : 0
return (messages || []).reduce((acc, item) => {
const token = buildRecruitMessageToken(item)
const itemTime = new Date(item?.createTime || 0).getTime()
if (clearedAt && itemTime && itemTime <= clearedAt) return acc
if ((item?.id && state.deletedMessageIds.includes(String(item.id))) || state.deletedTokens.includes(token)) return acc
const recalled = (item?.id && state.recalledMessageIds.includes(String(item.id))) || state.recalledTokens.includes(token)
acc.push(
recalled ? {
...item,
recalled: true,
recalledText: item.recalledText || '你撤回了一条消息',
content: '',
type: 0
} : item
)
return acc
}, [])
}
function filterRecruitFetchedMessages(key, messages = []) {
return applyRecruitStateToMessages(key, messages)
}
function upsertRecruitFetchedMessages(key, messages = [], options = {}) {
const state = readRecruitChatState(key)
const base = options.reset ? [] : [...state.messages]
messages.forEach((incoming) => {
const token = buildRecruitMessageToken(incoming)
const semanticKey = buildRecruitSemanticKey(incoming)
const idx = base.findIndex((item) =>
(incoming.id && item.id && String(item.id) === String(incoming.id)) ||
buildRecruitMessageToken(item) === token ||
(semanticKey && buildRecruitSemanticKey(item) === semanticKey) ||
canMergeRecruitMessages(item, incoming)
)
if (idx >= 0) base[idx] = mergeObject(base[idx], incoming)
else base.push(deepClone(incoming))
})
state.messages = sortRecruitMessages(applyRecruitStateToMessages(key, base))
writeRecruitChatState(key, state)
return state.messages
}
function clearRecruitChatHistory(key) {
const state = readRecruitChatState(key)
const ids = (state.messages || []).map((item) => item?.id ? String(item.id) : '').filter(Boolean)
const tokens = (state.messages || []).map((item) => buildRecruitMessageToken(item)).filter(Boolean)
state.deletedMessageIds = uniqueArray([...state.deletedMessageIds, ...ids])
state.deletedTokens = uniqueArray([...state.deletedTokens, ...tokens])
state.clearedBeforeTime = new Date().toISOString()
state.messages = []
writeRecruitChatState(key, state)
return []
}
function clearRecruitChatCache(key) {
safeRemoveStorage(stateStorageKey(key))
}
function persistBase() {
safeSetStorage(STORAGE_KEYS.identity, normalizeIdentity(identity.value))
safeSetStorage(STORAGE_KEYS.consumerId, String(consumerId.value || ''))
safeSetStorage(STORAGE_KEYS.userId, String(userId.value || ''))
safeSetStorage(STORAGE_KEYS.userState, String(userState.value || ''))
safeSetStorage(STORAGE_KEYS.organizationId, String(organizationId.value || ''))
safeSetStorage(STORAGE_KEYS.applicantId, String(applicantId.value || ''))
safeSetStorage(STORAGE_KEYS.organizationAvatarUrl, String(organizationAvatarUrl.value || ''))
safeSetStorage(STORAGE_KEYS.applicantAvatarUrl, String(applicantAvatarUrl.value || ''))
safeSetStorage(STORAGE_KEYS.organizationName, String(organizationName.value || ''))
safeSetStorage(STORAGE_KEYS.applicantName, String(applicantName.value || ''))
}
function hydrateFromStorage() {
identity.value = normalizeIdentity(safeGetStorage(STORAGE_KEYS.identity, 'enterprise'))
consumerId.value = String(safeGetStorage(STORAGE_KEYS.consumerId, '') || '')
userId.value = String(safeGetStorage(STORAGE_KEYS.userId, '') || '')
userState.value = String(safeGetStorage(STORAGE_KEYS.userState, '') || '')
organizationId.value = String(safeGetStorage(STORAGE_KEYS.organizationId, '') || '')
applicantId.value = String(safeGetStorage(STORAGE_KEYS.applicantId, '') || '')
organizationAvatarUrl.value = String(safeGetStorage(STORAGE_KEYS.organizationAvatarUrl, '') || '')
applicantAvatarUrl.value = String(safeGetStorage(STORAGE_KEYS.applicantAvatarUrl, '') || '')
organizationName.value = String(safeGetStorage(STORAGE_KEYS.organizationName, '') || '')
applicantName.value = String(safeGetStorage(STORAGE_KEYS.applicantName, '') || '')
enterpriseRegisterForm.value = mergeObject(createEnterpriseRegisterForm(), safeReadJSON(STORAGE_KEYS.enterpriseRegisterForm, createEnterpriseRegisterForm()) || {})
applicantRegisterForm.value = mergeObject(createApplicantRegisterForm(), safeReadJSON(STORAGE_KEYS.applicantRegisterForm, createApplicantRegisterForm()) || {})
editingPostDraft.value = safeReadJSON(STORAGE_KEYS.editingPostDraft, null)
const sessions = safeReadJSON(STORAGE_KEYS.recruitSessions, [])
recruitSessions.value = Array.isArray(sessions) ? sessions : []
textif (!organizationId.value && identity.value === 'enterprise' && applicantId.value) { identity.value = 'applicant' persistBase() } if (!applicantId.value && identity.value === 'applicant' && organizationId.value) { identity.value = 'enterprise' persistBase() }
}
function setIdentity(val) {
identity.value = normalizeIdentity(val)
persistBase()
}
function setConsumerId(val) {
consumerId.value = String(val || '')
persistBase()
}
function setUserId(val) {
userId.value = String(val || '')
persistBase()
}
function setUserState(val) {
userState.value = String(val || '')
persistBase()
}
function setOrganizationId(val) {
organizationId.value = String(val || '')
persistBase()
}
function setApplicantId(val) {
applicantId.value = String(val || '')
persistBase()
}
function setEnterpriseInfo(payload = {}) {
if (payload.identity !== undefined) identity.value = normalizeIdentity(payload.identity || 'enterprise')
if (payload.consumerId !== undefined) consumerId.value = String(payload.consumerId || '')
if (payload.userId !== undefined) userId.value = String(payload.userId || '')
if (payload.userState !== undefined) userState.value = String(payload.userState || '')
if (payload.organizationId !== undefined) organizationId.value = String(payload.organizationId || '')
if (payload.organizationName !== undefined) organizationName.value = String(payload.organizationName || '')
if (payload.name !== undefined && payload.organizationName === undefined) organizationName.value = String(payload.name || '')
if (payload.avatarUrl !== undefined) organizationAvatarUrl.value = String(payload.avatarUrl || '')
if (payload.organizationAvatarUrl !== undefined) organizationAvatarUrl.value = String(payload.organizationAvatarUrl || '')
persistBase()
}
function setApplicantInfo(payload = {}) {
if (payload.identity !== undefined) identity.value = normalizeIdentity(payload.identity || 'applicant')
if (payload.consumerId !== undefined) consumerId.value = String(payload.consumerId || '')
if (payload.userId !== undefined) userId.value = String(payload.userId || '')
if (payload.userState !== undefined) userState.value = String(payload.userState || '')
if (payload.applicantId !== undefined) applicantId.value = String(payload.applicantId || '')
if (payload.id !== undefined && payload.applicantId === undefined) applicantId.value = String(payload.id || '')
if (payload.realName !== undefined) applicantName.value = String(payload.realName || '')
if (payload.name !== undefined && payload.realName === undefined) applicantName.value = String(payload.name || '')
if (payload.avatarUrl !== undefined) applicantAvatarUrl.value = String(payload.avatarUrl || '')
if (payload.applicantAvatarUrl !== undefined) applicantAvatarUrl.value = String(payload.applicantAvatarUrl || '')
persistBase()
}
function updateOrganizationAvatar(url = '') {
organizationAvatarUrl.value = String(url || '')
persistBase()
}
function updateApplicantAvatar(url = '') {
applicantAvatarUrl.value = String(url || '')
persistBase()
}
function updateOrganizationName(name = '') {
organizationName.value = String(name || '')
persistBase()
}
function updateApplicantName(name = '') {
applicantName.value = String(name || '')
persistBase()
}
function patchEnterpriseRegisterForm(payload = {}) {
enterpriseRegisterForm.value = mergeObject(enterpriseRegisterForm.value, payload || {})
safeWriteJSON(STORAGE_KEYS.enterpriseRegisterForm, enterpriseRegisterForm.value)
}
function patchApplicantRegisterForm(payload = {}) {
applicantRegisterForm.value = mergeObject(applicantRegisterForm.value, payload || {})
safeWriteJSON(STORAGE_KEYS.applicantRegisterForm, applicantRegisterForm.value)
}
function resetEnterpriseRegisterForm() {
enterpriseRegisterForm.value = createEnterpriseRegisterForm()
safeWriteJSON(STORAGE_KEYS.enterpriseRegisterForm, enterpriseRegisterForm.value)
}
function resetApplicantRegisterForm() {
applicantRegisterForm.value = createApplicantRegisterForm()
safeWriteJSON(STORAGE_KEYS.applicantRegisterForm, applicantRegisterForm.value)
}
function setEditingPostDraft(payload = null) {
editingPostDraft.value = payload ? deepClone(payload) : null
if (editingPostDraft.value) safeWriteJSON(STORAGE_KEYS.editingPostDraft, editingPostDraft.value)
else safeRemoveStorage(STORAGE_KEYS.editingPostDraft)
}
function clearEditingPostDraft() {
setEditingPostDraft(null)
}
function clearEnterpriseIdentity() {
organizationId.value = ''
organizationAvatarUrl.value = ''
organizationName.value = ''
resetEnterpriseRegisterForm()
safeRemoveStorage(STORAGE_KEYS.organizationId)
safeRemoveStorage(STORAGE_KEYS.organizationAvatarUrl)
safeRemoveStorage(STORAGE_KEYS.organizationName)
safeRemoveStorage(STORAGE_KEYS.enterpriseRegisterForm)
if (identity.value === 'enterprise') identity.value = applicantId.value ? 'applicant' : 'enterprise'
persistBase()
}
function clearApplicantIdentity() {
applicantId.value = ''
applicantAvatarUrl.value = ''
applicantName.value = ''
resetApplicantRegisterForm()
safeRemoveStorage(STORAGE_KEYS.applicantId)
safeRemoveStorage(STORAGE_KEYS.applicantAvatarUrl)
safeRemoveStorage(STORAGE_KEYS.applicantName)
safeRemoveStorage(STORAGE_KEYS.applicantRegisterForm)
if (identity.value === 'applicant') identity.value = organizationId.value ? 'enterprise' : 'enterprise'
persistBase()
}
function saveToStorage() {
persistBase()
safeWriteJSON(STORAGE_KEYS.enterpriseRegisterForm, enterpriseRegisterForm.value)
safeWriteJSON(STORAGE_KEYS.applicantRegisterForm, applicantRegisterForm.value)
safeWriteJSON(STORAGE_KEYS.recruitSessions, recruitSessions.value)
if (editingPostDraft.value) safeWriteJSON(STORAGE_KEYS.editingPostDraft, editingPostDraft.value)
}
function clearAll() {
identity.value = 'enterprise'
consumerId.value = ''
userId.value = ''
userState.value = ''
organizationId.value = ''
applicantId.value = ''
organizationAvatarUrl.value = ''
applicantAvatarUrl.value = ''
organizationName.value = ''
applicantName.value = ''
enterpriseRegisterForm.value = createEnterpriseRegisterForm()
applicantRegisterForm.value = createApplicantRegisterForm()
recruitSessions.value = []
editingPostDraft.value = null
Object.values(STORAGE_KEYS).forEach((key) => safeRemoveStorage(key))
}
hydrateFromStorage()
return {
identity,
consumerId,
userId,
userState,
organizationId,
applicantId,
recrueitJobApplicantId,
isEnterprise,
isApplicant,
currentAvatarUrl,
currentDisplayName,
organizationAvatarUrl,
applicantAvatarUrl,
organizationName,
applicantName,
enterpriseRegisterForm,
applicantRegisterForm,
editingPostDraft,
recruitSessions,
hydrateFromStorage,
saveToStorage,
clearAll,
setIdentity,
setConsumerId,
setUserId,
setUserState,
setOrganizationId,
setApplicantId,
setEnterpriseInfo,
setEnterpriseProfile: setEnterpriseInfo,
setApplicantInfo,
setApplicantProfile: setApplicantInfo,
updateOrganizationAvatar,
updateEnterpriseAvatar: updateOrganizationAvatar,
updateApplicantAvatar,
updateOrganizationName,
updateApplicantName,
patchEnterpriseRegisterForm,
patchApplicantRegisterForm,
resetEnterpriseRegisterForm,
resetApplicantRegisterForm,
setEditingPostDraft,
clearEditingPostDraft,
clearEnterpriseIdentity,
clearApplicantIdentity,
makeRecruitChatCacheKey,
makeRecruitSessionKey,
registerRecruitSession,
touchRecruitSession,
listRecruitSessions,
markRecruitSessionRead,
bumpRecruitSessionUnread,
removeRecruitSession,
readRecruitChatState,
readRecruitChatCache,
saveRecruitChatCache,
appendRecruitChatMessage,
deleteRecruitChatMessage,
recallRecruitChatMessage,
filterRecruitFetchedMessages,
upsertRecruitFetchedMessages,
clearRecruitChatHistory,
clearRecruitChatCache
}
})
参考这个咖募
1.recrueitTalkTakenotes招募聊天
入口参数
String consumerId,人id
String recrueitPostId, 职位id
Boolean consumerTalk,是否是求职者发送
String content内容
Integer type 文件类型e
String showName 文件显示名称
recrueitFile 上传文件名
String resumeId 简历id
返回值
id
2.recrueitTalkTakenotesList咖募聊天列表
入口参数
String consumerId, 用户id
String recruitPostId, 职位id
Integer rowStartIdx, 起始行
Integer rowCount 行数
返回值
talkArray 聊天列表
id
content内容
consumerTalk是否是求职者发言
consumerId 用户id
createTime 建立时间
3.recruitPostListByRecrueitTalkTakenotes 求职者聊过天的职位
入口参数
String consumerId,用户id
Integer rowStartIdx, 起始行
Integer rowCount行数
返回值
postArray 职位列表
id
title标题
position职位
organizationName招聘单位名称 content最后一条聊天内容
createTime最后一条聊天时间
4.webSocket长链接接口
{“type”:”recrueit”,”id”:”xxxxxxxx”}json转成的字符串格式 处理咖募聊天
接受到正确的格式message后会向组内所有在线用户或发送消息,消息格式:
[
content:“XXX” 聊天内容
type:“5”(0 文字 1 图片 2 视频 3 声音4 文件 5 个人简历 6 面试邀请 7 答题)
consumerTalk 是否是求职者发言
fileName:“xxx”
showName:“xxx”
resumeId:简历id
recrueitInterviewId:面试id
recrueitQuestionGroupId:答题id
playURL:“xxx”
consumerId:"xxx" 发送者
createTime: 建立时间
]
5.recrueitPost 招聘岗位维护
入口参数
String id
String title, 招募标题
String position, 职位名称
String department, 部门
String jobType, 工作类型(全职/兼职)
Integer workExperience, 工作经验要求
Integer education, 学历要求
String salaryRange, 薪资范围
String location, 工作地点
String longitude, 经度
String latitude, 纬度
String description, 岗位描述
String requirements, 任职
String benefits, 福利待遇
Date deadline, 招聘截止日期
String contactPerson, 联系人
String phone, 联系电话
String organizationId, 招聘公司id
Integer state, 状态(初始1,确认2,拒绝-1)
String creatorId, 创建人id
返回值
id
6.recrueitPostAudit 岗位审核
入口参数
String id
Integer state, 状态(初始1,确认2,拒绝-1)
String confirmContent, 审批理由(当state = -1时必填)
7.recrueitPostAuditList 岗位审核列表
入口参数
Integer rowStartIdx, 起始行
Integer rowCount 行数
返回值
recrueitPostArray
id
title, 招募标题
position, 职位名称
department, 部门
jobType, 工作类型(全职/兼职)
workExperience, 工作经验要求
education, 学历要求
salaryRange, 薪资范围
location, 工作地点
longitude, 经度
latitude, 纬度
description, 岗位描述
requirements, 任职
benefits, 福利待遇
deadline, 招聘截止日期
contactPerson, 联系人
phone, 联系电话
organizationId, 招聘公司id
state, 状态(初始1,确认2,拒绝-1)
creatorId, 创建人id
createTime 创建时间
8.recrueitPostList 招聘岗位列表
入口参数
String organizationId, 招聘公司id(不传id就查全部)
Integer rowStartIdx, 起始行
Integer rowCount行数
String keyword, 关键词(搜索时用)
返回值
recrueitPostArray
id
title, 招募标题
position, 职位名称
department, 部门
jobType, 工作类型(全职/兼职)
workExperience, 工作经验要求
education, 学历要求
salaryRange, 薪资范围
location, 工作地点
longitude, 经度
latitude, 纬度
description, 岗位描述
requirements, 任职
benefits, 福利待遇
deadline, 招聘截止日期
contactPerson, 联系人
phone, 联系电话
organizationId, 招聘公司id
state, 状态(初始1,确认2,拒绝-1)
creatorId, 创建人id
confirmContent 审批理由
createTime 创建时间
avatarUrl 企业头像
nickname 企业名称
enterpriseScale 企业规模
9.companyAllRecrueitPostList 公司岗位
入口参数
String organizationId, 招聘公司id
Integer rowStartIdx, 起始行
Integer rowCount行数
返回值
recrueitPostArray
id
title, 招募标题
position, 职位名称
department, 部门
jobType, 工作类型(全职/兼职)
workExperience, 工作经验要求
education, 学历要求
salaryRange, 薪资范围
location, 工作地点
longitude, 经度
latitude, 纬度
description, 岗位描述
requirements, 任职
benefits, 福利待遇
deadline, 招聘截止日期
contactPerson, 联系人
phone, 联系电话
organizationId, 招聘公司id
state, 状态(0未发布,1发布)
creatorId, 创建人id
confirmContent 审批理由
createTime 创建时间
10.deleteRecrueitPost 删除招聘岗位
入口参数
String id
String creatorId, 操作者id
11.recrueitPostShow 招聘岗位详情
入口参数
Sting id
返回值
id
title, 招募标题
position, 职位名称
department, 部门
jobType, 工作类型(全职/兼职)
workExperience, 工作经验要求
education, 学历要求
salaryRange, 薪资范围
location, 工作地点
longitude, 经度
latitude, 纬度
description, 岗位描述
requirements, 任职
benefits, 福利待遇
deadline, 招聘截止日期
contactPerson, 联系人
phone, 联系电话
organizationId, 招聘公司id
state, 状态(0未发布,1发布)
creatorId, 创建人id
createTime, 发布时间
12.resumeList 求职者简历列表
入口参数
Integer rowStartIdx, 起始行
Integer rowCount行数
String keyword, 关键词(搜索时用)
返回值
String id
String name, 姓名
String avatarUrl, 头像
String education, 学历
String performerId, 个人简历id
String position, 求职岗位
Integer state, 状态
String currentAddress, 当前地址
String salaryExpectation, 期望薪资
13.RecrueitJobApplicant 求职者信息维护
入口参数
String id
String name 昵称
String avatarUrl, 头像
String education, 学历
String performerId, 个人简历id
String position, 求职岗位
Integer state, 状态
String currentAddress, 当前地址
String salaryExpectation, 期望薪资
返回值
id
14.RecrueitJobApplicantDelete 求职者信息注销
入口参数
String id 求职者id
15.recrueitJobApplicantUpdateAvatarUr求职者头像上
入口参数
String recrueitJobApplicantId, 求职者id
avatarFile, 文件名
返回值
avatarUrl 机构头像Url
16.RecrueitJobApplicantShow, 求职者信息详情
入口参数
String id
返回值
String id
String name, 姓名
String avatarUrl, 头像
String education, 学历
String performerId, 个人简历id
String position, 求职岗位
Integer state, 状态
String currentAddress, 当前地址
String salaryExpectation, 期望薪资
17.recrueitInterview 面试邀请
入口参数
String id
String recrueitPostId, 岗位id
String applicantId, 应聘者id
Date interviewTime, 面试时间
String interviewLocation, 面试地点
String longitude, 经度
String latitude, 纬度
Integer state, 面试状态(初始1,确认2,拒绝-1)
Boolean Accept, 是否接受面试
String creatorId, 创建者id
返回值
id
18.recrueitInterviewShow 面试邀请详情
入口参数
String id
返回值
id
recrueitPostId, 岗位id
applicantId, 应聘者id
performerId, 应聘者简历id
interviewTime, 面试时间
interviewLocation, 面试地点
longitude, 经度
latitude, 纬度
state, 面试状态(初始1,确认2,拒绝-1)
Accept, 是否接受面试
creatorId, 创建者id
19.recrueitInterviewLists 公司收到的面试记录
入口参数
String organizationId, 公司id
String consumerId, 公司员工id
Integer rowStartIdx, 起始行
Integer rowCount行数
返回值
recrueitInterviewArray
id
recrueitPostId, 岗位id
applicantId, 应聘者id
interviewTime, 面试时间
interviewLocation, 面试地点
longitude, 经度
latitude, 纬度
state, 面试状态(初始1,确认2,拒绝-1)
Accept, 是否接受面试
creatorId, 创建者id
20.myRecrueitInterviewList 本人的面试记录
入口参数
String applicantId, 求职者id
Integer rowStartIdx, 起始行
Integer rowCount行数
返回值
recrueitInterviewArray
id
recrueitPostId, 岗位id
applicantId, 应聘者id
interviewTime, 面试时间
interviewLocation, 面试地点
longitude, 经度
latitude, 纬度
state, 面试状态(初始1,确认2,拒绝-1)
Accept, 是否接受面试
creatorId, 创建者id
21.recrueitQuestionGroup 题目组维护
入口参数
String id,
String title, 题目组标题
String consumerId, 创建者ID
String[] questionIds, 题目ID数组
String[] questionTitles, 题目标题数组
String[] optionIds, 选项ID数组
String[] optionTitles, 选项标题数组
Integer[] optionQuestionIndexes, 选项对应题目索引
String[] updateQuestionIds, 需要更新的题目ID
String[] updateOptionIds, 需要更新的选项ID
返回值
id
22.recrueitQuestionGroupShow 题目组详情
入口参数
String id
返回值
id
title, 题目组标题
consumerId, 创建者id
createTime, 建立时间
questionsArray题目列表
id
title, 题目
createTime, 创建时间
questionGroupId, 所属组id
optionsArray, 题目下的选项列表
id
title, 题目 createTime, 创建时间
questionId, 所属题目id
23.deleteRecrueitQuestionGroup 删除题目组
入口参数
String id
String consumerId, 创建者id
24.deleteRecrueitQuestion 删除题目
入口参数
String questionId, 题目id
String consumerId, 创建者id
25.deleteRecrueitOption 删除选项
入口参数
String optionId, 选项id
String consumerId, 创建者id
26.myRecrueitQuestionGroupList 本人的题目组列表
入口参数
String consumerId, 创建者id
Integer rowStartIdx, 起始行
Integer rowCount 行数
返回值
questionGroupArray,题目组列表
id
title, 题目组标题
createTime, 建立时间
consumerId, 创建者id
questionCount, 题目数量
27.recrueitAnswer 答题
入口参数
String consumerId, 应聘者id
String recrueitPostId, 应聘岗位id
String recrueitQuestionId, 问题id String recrueitOptionId,选项id
String content, 内容
Boolean jobAnswer,是否是应聘人回答
返回值
id
28.recrueitAnswerList 答题记录
入口参数
String consumerId, 应聘者id
String recrueitPostId, 职位id
Integer rowStartIdx, 起始行
Integer rowCount 行数
返回值
answerArray答题纪录列表
id
content, 内容
jobAnswer, 是否是应聘人回答
consumerId, 应聘者id
recrueitQuestionId, 问题id
recrueitOptionId, 选项id
createTime, 答题时间
29.newOrganizationRegister 商家注册
入口参数
String password 密码(当organization.type=2时用)
String confirmPassword 确认密码(当organization.type=2时用)
C Common organizationFileList 机构文件
Common fileIndexList 文件是否上传
Organization organization 机构对象
name 名称
phoneticism 拼音
shortName 简称
setUpTime 创建时间
taxpayerIdentityNumber 纳税人识别号
province 省
city 市
area 地区
state 状态(-1拒绝,0维护,1上报,2有效,-1停用,-2删除,)
type 类型(1预警,2社区救援仓,3咖招)
address 地址
taxpayerIdentityNumber 统一信用社代码
aboutUs 公司理念
setUpTime 成立时间
enterpriseScale 公司规模
companyProfile 公司理念
isExRegist 注册身份
industry 所属行业
latitude精度
longitude 维度
bank 开户行
bankAccount 开户行账号
introduction简介
aboutUs 关于我们
organizationAttribute 机构属性(个人、企业)
Personnel personnel 人员对象
User user
username 用户名 等于手机号
textmobilePhone 手机号 address同organization的address name 姓名 idCard 身份证号 String currentOrganizationTypeId 当前组织属性id
返回值
userState 用户状态
userId 用户id
organizationId机构id
consumerId
30.deleteOrganization 机构注销
入口参数
String organizationId
31.updateOrganization 机构信息修改
入口参数
String organizationId, 机构id
String officeEnvironment, 办公环境
String address, 面试地址
String name, 公司称呼
String consumerId 用户id
String mobilePhone 手机号
返回值
result success
32.organizationFile 机构图片上传
入口参数
organizationFile 文件名
String organizationId 机构id
返回值
uploadedFileId 文件id
fileName 文件名
fileUrl 文件URL
33.organizationUpdateAvatarUrl 机构头像上传
入口参数
String organizationId, 机构id
avatarFile, 文件名
返回值
avatarUrl 机构头像Url
34.organizationAuditList 机构审核列表
入口参数
Integer rowStartIdx, 起始行
Integer rowCount 行数
Integer type (1预警,2社区救援仓,3咖招)
返回值
organizationArray
id 机构id
organizationName 机构名
state 状态
createTime 创建时间
type 类型
address 地址
taxpayerIdentityNumber 统一信用社代码
aboutUs 公司理念
setUpTime 成立时间
enterpriseScale 公司规模
isExRegist 注册身份
industry 所属行业
35.organizationDetail 机构详情
入口参数
String organizationId, 机构id
Integer type (1预警,2社区救援仓,3咖招)
返回值
organizationId: 机构id
name 机构名
state 状态
confirmContent 审批理由
taxpayerIdentityNumber: 统一社会信用代码
address: 机构地址
officeEnvironment 办公环境
companyProfile 公司理念
industry 所属行业
introduction: 机构介绍
createTime 创建时间
avatarUrl 机构头像Url
systemNumber 咖值号
creatorId, 创建人id
organizationAttributeId: 机构属性的唯一标识符
organizationAttributeName: 机构属性名称
organizationAttributeCode: 机构属性编码
personnelArray
personnelId: 相关人员id
personnelName: 人员姓名
mobilePhone: 手机号码
email: 邮箱地址
duty: 职务信息
organizationFileArray
id 文件id
fileName 文件名
36.newOrganizationAudit机构审批
入口参数
String organizationId, 机构id
Integer state, 状态(-1拒绝,0维护,1上报,2有效,-1停用,-2删除,)
String confirmContent, 审批理由
简历模板
1.简历模板的维护与上传 resumeTemplate
入口参数
String consumerId, 上传的用户id
String templateName, 模板名
String htmlContent, 副本内容
file 文件名
返回值
templateId 模板id
templateName 模板名
filePath 文件路径
2.简历模板列表 resumeTemplateList
入口参数
Integer rowStartIdx, 起始行
Integer rowCount 行数
返回值
id
createConsumerId 上传用户id
state 状态
templateName 模板名
fileName 文件名
filePath 文件路径
createTime 创建时间
3.删除简历模板deleteResumeTemplate
入口参数
String templateId 模板id
4.简历模板详情 resumeTemplateShow
入口参数
String templateId 模板id
返回值
id
createConsumerId 上传用户id
state 状态
templateName 模板名
fileName 文件名
filePath 文件路径
createTime 创建时间
5.简历模板审核 resumeTemplateAudit
入口参数
String resumeTemplateId 模板id
Integer state, 状态(1初始,2通过,-1未通过)
6.简历模板审核列表 resumeTemplateAuditList
入口参数
Integer rowStartIdx, 起始行
Integer rowCount 行数
返回值
templateList
id
createConsumerId 上传用户id
state 状态
templateName 模板名
fileName 文件名
filePath 文件路径
createTime 创建时间
在参考这个页面<template>
<view :class="['app-container', theme]">
<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }">
<view class="header-inner">
<view class="left-box" @click="navigateBack">
<uni-icons type="left" color="#1d1d1f" size="24"></uni-icons>
</view>
<view class="center-box">
<text class="title-text">加入咖招</text>
</view>
<view class="right-box"></view>
</view>
</view>
</template> <script setup> import { ref, reactive, computed, inject, onUnmounted } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { useUserecruitmentsharingStore } from '@/store/Userecruitmentsharing.js' import { onLoad } from '@dcloudio/uni-app' import ajax from '@/api/ajax.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' const LOCATION_PICKER_URL = '/pages/sociAlize/sharedspace/sharedspace' const BACK_HOME_URL = '/pages/sociAlize/Coffeemu/CoffeemuHome' const POST_POSITION_URL = '/pages/sociAlize/Coffeemu/Postposition/Postposition' const mainStore = useMainStore() const recruitStore = useUserecruitmentsharingStore() const { theme } = storeToRefs(mainStore) const statusBarHeight = ref(Number(getStatusBarHeight?.() || uni.getSystemInfoSync()?.statusBarHeight || 0)) const headerBarHeight = ref(Number(getTitleBarHeight?.() || 44)) const headerTotalPx = computed(() => Number(statusBarHeight.value || 0) + Number(headerBarHeight.value || 0)) const isEnterpriseMode = ref(true) const submitting = ref(false) const countdown = ref(0) const verifyCode = ref('') const sentCode = ref('') let timer = null const injectedServerUrl = inject('serverUrl', '') || inject('webProjectUrl', '') || '' const industryOptions = ['互联网', '餐饮', '教育培训', '传媒广告', '制造业', '电商零售', '服务业', '其他'] const scaleOptions = ['20-100人', '100-300人', '300-500人', '500-1000人', '1000人以上'] const educationOptions = ['初中及以下', '中专/高中', '大专', '本科', '硕士', '博士'] const jobStateOptions = ['随时到岗', '周内到岗', '月内到岗', '考虑机会', '暂不考虑'] const industryIndex = ref(-1) const scaleIndex = ref(-1) const educationIndex = ref(-1) const jobStateIndex = ref(-1) const currentIndustry = computed(() => (industryIndex.value > -1 ? industryOptions[industryIndex.value] : '')) const currentScale = computed(() => (scaleIndex.value > -1 ? scaleOptions[scaleIndex.value] : '')) const currentJobState = computed(() => (jobStateIndex.value > -1 ? jobStateOptions[jobStateIndex.value] : '')) const activeAvatarPreview = computed(() => (isEnterpriseMode.value ? enterpriseAvatarFile.previewUrl : applicantAvatarFile.previewUrl)) const enterpriseForm = reactive({ password: '', confirmPassword: '', organizationFileList: [], fileIndexList: [], currentOrganizationTypeId: '', organization: { name: '', phoneticism: '', shortName: '', setUpTime: '', taxpayerIdentityNumber: '', province: '', city: '', area: '', state: 1, type: 3, address: '', aboutUs: '', enterpriseScale: '', companyProfile: '', isExRegist: '咖招', industry: '', latitude: '', longitude: '', bank: '', bankAccount: '', introduction: '', organizationAttribute: '企业' }, personnel: { mobilePhone: '', address: '', name: '', idCard: '', email: '', duty: '' }, user: { username: '', mobilePhone: '', address: '', name: '', idCard: '' } }) const applicantForm = reactive({ realName: '', avatarUrl: '', education: '', position: '', state: 1, currentAddress: '', salaryExpectation: '' }) const enterpriseAvatarFile = reactive({ tempFilePath: '', previewUrl: '' }) const applicantAvatarFile = reactive({ tempFilePath: '', previewUrl: '' }) const licenseFile = reactive({ tempFilePath: '', previewUrl: '' }) const selectedLocation = ref({ name: '', address: '', latitude: '', longitude: '', province: '', city: '', district: '' }) function toast(title) { uni.showToast({ title: String(title || ''), icon: 'none' }) } function safeDecode(v) { let s = String(v ?? '') for (let i = 0; i < 2; i++) { if (!/%[0-9A-Fa-f]{2}/.test(s)) break try { s = decodeURIComponent(s) } catch (e) { break } } return s } function navigateBack() { const pages = getCurrentPages() if (pages.length > 1) { uni.navigateBack({ fail: () => { uni.redirectTo({ url: BACK_HOME_URL }) } }) return } uni.redirectTo({ url: BACK_HOME_URL }) } function setIdentityMode(isEnterprise) { isEnterpriseMode.value = isEnterprise recruitStore.setIdentity(isEnterprise ? 'enterprise' : 'applicant') } function onIndustryChange(e) { industryIndex.value = Number(e.detail.value) enterpriseForm.organization.industry = industryOptions[industryIndex.value] } function onScaleChange(e) { scaleIndex.value = Number(e.detail.value) enterpriseForm.organization.enterpriseScale = scaleOptions[scaleIndex.value] } function onEducationChange(e) { educationIndex.value = Number(e.detail.value) applicantForm.education = educationOptions[educationIndex.value] } function onJobStateChange(e) { jobStateIndex.value = Number(e.detail.value) applicantForm.state = jobStateIndex.value + 1 } function openLocation() { uni.navigateTo({ url: LOCATION_PICKER_URL, success: (navRes) => { navRes.eventChannel?.on('sendLocation', (loc) => { selectedLocation.value = { name: loc?.name || '', address: loc?.address || loc?.name || '', latitude: loc?.latitude || '', longitude: loc?.longitude || '', province: loc?.province || '', city: loc?.city || '', district: loc?.district || '' } Object.assign(enterpriseForm.organization, { address: selectedLocation.value.address, latitude: selectedLocation.value.latitude, longitude: selectedLocation.value.longitude, province: selectedLocation.value.province, city: selectedLocation.value.city, area: selectedLocation.value.district }) enterpriseForm.personnel.address = selectedLocation.value.address enterpriseForm.user.address = selectedLocation.value.address }) } }) } function openApplicantLocation() { uni.navigateTo({ url: LOCATION_PICKER_URL, success: (navRes) => { navRes.eventChannel?.on('sendLocation', (loc) => { applicantForm.currentAddress = loc?.address || loc?.name || '' }) } }) } function pickImage(slot) { uni.chooseImage({ count: 1, success: (res) => { const path = res.tempFilePaths?.[0] if (path) { slot.tempFilePath = path slot.previewUrl = path } } }) } function isValidPhone(phone) { return /^1[3-9]\d{9}$/.test(String(phone || '').trim()) } function getApiData(res) { const root = res?.data ?? res ?? {} return root?.data && typeof root.data === 'object' ? root.data : root } function encodeForm(data) { const flat = {} function flatten(obj, prefix = '') { if (Array.isArray(obj)) { obj.forEach((item, index) => { const nextKey = `${prefix}[${index}]` if (item && typeof item === 'object') { flatten(item, nextKey) } else { flat[nextKey] = item ?? '' } }) return } Object.keys(obj || {}).forEach((key) => { const val = obj[key] const newKey = prefix ? `${prefix}.${key}` : key if (Array.isArray(val)) { flatten(val, newKey) } else if (val && typeof val === 'object') { flatten(val, newKey) } else if (val !== undefined && val !== null) { flat[newKey] = String(val) } }) } flatten(data) return Object.keys(flat) .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(flat[k])}`) .join('&') } function normalizeBaseUrl(url) { return String(url || '').replace(/\/$/, '') } function uploadFileCommon(urlName, nameField, formData, filePath) { return new Promise((resolve, reject) => { const base = normalizeBaseUrl(injectedServerUrl) uni.uploadFile({ url: `${base}/${urlName}`, filePath, name: nameField, formData, header: ajax?.getHeaders?.() || {}, success: (res) => { try { const json = typeof res.data === 'string' ? JSON.parse(res.data) : res.data resolve(json) } catch (e) { reject(e) } }, fail: reject }) }) } async function sendEnterpriseCode() { const phone = String(enterpriseForm.personnel.mobilePhone || '').trim() if (!isValidPhone(phone)) { toast('请输入正确的手机号') return } try { const res = await ajax.post({ url: 'getRandom.do', data: { mobilePhone: phone } }) const data = getApiData(res) if (String(data?.result || '').toLowerCase() === 'success') { sentCode.value = String(data.random || '') countdown.value = 60 if (timer) clearInterval(timer) timer = setInterval(() => { if (countdown.value > 0) countdown.value-- else { clearInterval(timer) timer = null } }, 1000) toast('验证码已发送') } else { toast(data?.message || '发送失败') } } catch (e) { toast('发送失败') } } function validateEnterprise() { if (!enterpriseForm.organization.name) return toast('请输入企业名称') if (industryIndex.value < 0) return toast('请选择所属行业') if (scaleIndex.value < 0) return toast('请选择企业规模') if (!enterpriseForm.personnel.name) return toast('请输入管理员称呼') if (!enterpriseForm.personnel.idCard) return toast('请输入管理员身份证号') // if (!isValidPhone(enterpriseForm.personnel.mobilePhone)) return toast('请输入正确的手机号') // if (!verifyCode.value) return toast('请输入短信验证码') // if (sentCode.value && String(verifyCode.value) !== String(sentCode.value)) return toast('验证码错误') // if (!enterpriseForm.password || enterpriseForm.password.length < 6) return toast('请设置不少于6位的密码') // if (enterpriseForm.password !== enterpriseForm.confirmPassword) return toast('两次输入的密码不一致') // if (!enterpriseForm.organization.taxpayerIdentityNumber) return toast('请输入统一社会信用代码') // if (!selectedLocation.value.address) return toast('请选择面试地址') return true } function validateApplicant() { if (!applicantForm.realName) return toast('请输入真实姓名') if (!applicantForm.education) return toast('请选择学历') if (!applicantForm.position) return toast('请输入期望职位') return true } async function submitEnterprise() { if (!validateEnterprise()) return const phone = String(enterpriseForm.personnel.mobilePhone || '').trim() enterpriseForm.user.username = phone enterpriseForm.user.mobilePhone = phone enterpriseForm.user.name = enterpriseForm.personnel.name enterpriseForm.user.idCard = enterpriseForm.personnel.idCard enterpriseForm.user.address = enterpriseForm.organization.address enterpriseForm.personnel.mobilePhone = phone enterpriseForm.personnel.address = enterpriseForm.organization.address enterpriseForm.organization.setUpTime = new Date().toISOString().slice(0, 10) enterpriseForm.organization.state = 1 enterpriseForm.organization.type = 3 enterpriseForm.organization.isExRegist = '咖招' enterpriseForm.organization.organizationAttribute = '企业' enterpriseForm.organization.companyProfile = enterpriseForm.organization.aboutUs || '' enterpriseForm.organization.introduction = enterpriseForm.organization.aboutUs || '' const payload = { password: enterpriseForm.password, confirmPassword: enterpriseForm.confirmPassword, organizationFileList: [], fileIndexList: [], currentOrganizationTypeId: enterpriseForm.currentOrganizationTypeId || '', organization: { ...enterpriseForm.organization }, personnel: { ...enterpriseForm.personnel }, user: { ...enterpriseForm.user } } recruitStore.patchEnterpriseRegisterForm(payload) const res = await ajax.post({ url: 'newOrganizationRegister', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: encodeForm(payload) }) const data = getApiData(res) const newOrgId = String(data?.organizationId || '') const newConsumerId = String(data?.consumerId || '') const newUserId = String(data?.userId || '') const newUserState = String(data?.userState || '') if (!newOrgId) { throw new Error(data?.message || '企业注册失败') } if (!newConsumerId) { throw new Error('注册成功但未返回 consumerId,无法继续发岗位') } recruitStore.setEnterpriseInfo({ identity: 'enterprise', consumerId: newConsumerId, userId: newUserId, userState: newUserState, organizationId: newOrgId, name: enterpriseForm.organization.name }) let uploadedAvatarUrl = '' if (enterpriseAvatarFile.tempFilePath) { try { const avatarRes = await uploadFileCommon( 'organizationUpdateAvatarUrl', 'avatarFile', { organizationId: newOrgId }, enterpriseAvatarFile.tempFilePath ) uploadedAvatarUrl = avatarRes?.avatarUrl || '' if (uploadedAvatarUrl) { recruitStore.updateOrganizationAvatar(uploadedAvatarUrl) } } catch (e) {} } if (licenseFile.tempFilePath) { try { await uploadFileCommon( 'organizationFile', 'organizationFile', { organizationId: newOrgId }, licenseFile.tempFilePath ) } catch (e) {} } try { await ajax.post({ url: 'newOrganizationAudit', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: encodeForm({ organizationId: newOrgId, state: 1, confirmContent: '' }) }) } catch (e) {} recruitStore.saveToStorage() uni.showToast({ title: '入驻成功', icon: 'success' }) setTimeout(() => { uni.redirectTo({ url: `${POST_POSITION_URL}?organizationId=${encodeURIComponent(newOrgId)}&creatorId=${encodeURIComponent(newConsumerId)}` }) }, 800) } async function submitApplicant() { if (!validateApplicant()) return const payload = { id: '', name: applicantForm.realName, avatarUrl: '', education: applicantForm.education, performerId: '', position: applicantForm.position, state: applicantForm.state, currentAddress: applicantForm.currentAddress, salaryExpectation: applicantForm.salaryExpectation } recruitStore.patchApplicantRegisterForm(payload) const res = await ajax.post({ url: 'RecrueitJobApplicant', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: encodeForm(payload) }) const data = getApiData(res) const newAppId = String(data?.id || '') if (!newAppId) { throw new Error(data?.message || '求职者注册失败') } recruitStore.setApplicantInfo({ identity: 'applicant', consumerId: recruitStore.consumerId || uni.getStorageSync('consumerId') || '', applicantId: newAppId, name: applicantForm.realName }) if (applicantAvatarFile.tempFilePath) { try { const avatarRes = await uploadFileCommon( 'recrueitJobApplicantUpdateAvatarUr', 'avatarFile', { recrueitJobApplicantId: newAppId }, applicantAvatarFile.tempFilePath ) if (avatarRes?.avatarUrl) { recruitStore.updateApplicantAvatar(avatarRes.avatarUrl) } } catch (e) {} } recruitStore.saveToStorage() uni.showToast({ title: '注册成功', icon: 'success' }) setTimeout(() => { uni.redirectTo({ url: BACK_HOME_URL }) }, 800) } async function submitAll() { if (submitting.value) return submitting.value = true uni.showLoading({ title: '正在提交...', mask: true }) try { if (isEnterpriseMode.value) { await submitEnterprise() } else { await submitApplicant() } } catch (e) { toast(String(e?.message || '网络异常')) } finally { uni.hideLoading() submitting.value = false } } onLoad((option) => { recruitStore.hydrateFromStorage() const reqIdentity = safeDecode(option?.identity || '') if (reqIdentity === 'applicant') isEnterpriseMode.value = false else if (reqIdentity === 'enterprise') isEnterpriseMode.value = true const savedEnterprise = recruitStore.enterpriseRegisterForm || {} const savedApplicant = recruitStore.applicantRegisterForm || {} if (savedEnterprise.organization?.name) { Object.assign(enterpriseForm, savedEnterprise) const savedIndustry = String(savedEnterprise.organization?.industry || '') const savedScale = String(savedEnterprise.organization?.enterpriseScale || '') industryIndex.value = industryOptions.findIndex((item) => item === savedIndustry) scaleIndex.value = scaleOptions.findIndex((item) => item === savedScale) } if (savedApplicant.name || savedApplicant.position) { applicantForm.realName = savedApplicant.name || '' applicantForm.education = savedApplicant.education || '' applicantForm.position = savedApplicant.position || '' applicantForm.state = Number(savedApplicant.state || 1) applicantForm.currentAddress = savedApplicant.currentAddress || '' applicantForm.salaryExpectation = savedApplicant.salaryExpectation || '' educationIndex.value = educationOptions.findIndex((item) => item === applicantForm.education) jobStateIndex.value = Number(applicantForm.state || 1) - 1 } }) onUnmounted(() => { if (timer) clearInterval(timer) recruitStore.patchEnterpriseRegisterForm(enterpriseForm) recruitStore.patchApplicantRegisterForm({ id: '', name: applicantForm.realName, avatarUrl: '', education: applicantForm.education, performerId: '', position: applicantForm.position, state: applicantForm.state, currentAddress: applicantForm.currentAddress, salaryExpectation: applicantForm.salaryExpectation }) }) </script> <style lang="scss" scoped> .app-container { min-height: 100vh; background: #f7f8fa; } .header { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(247, 248, 250, 0.9); backdrop-filter: blur(10px); } .header-inner { display: flex; align-items: center; justify-content: space-between; height: 100%; padding: 0 30rpx; } .left-box, .right-box { width: 80rpx; display: flex; align-items: center; } .left-box { justify-content: flex-start; } .right-box { justify-content: flex-end; } .title-text { font-size: 34rpx; font-weight: 600; color: #1d1d1f; } .page-scroll { position: absolute; left: 0; right: 0; bottom: 0; } .container { padding: 30rpx; } .identity-switcher { display: flex; background: #eef0f4; border-radius: 16rpx; padding: 6rpx; margin-bottom: 40rpx; } .switch-item { flex: 1; height: 76rpx; display: flex; align-items: center; justify-content: center; gap: 12rpx; border-radius: 12rpx; font-size: 28rpx; color: #86868b; font-weight: 500; transition: all 0.3s; } .switch-item.is-active { background: #ffffff; color: #1d1d1f; box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06); } .avatar-section { display: flex; flex-direction: column; align-items: center; margin-bottom: 40rpx; } .avatar-upload { position: relative; width: 160rpx; height: 160rpx; border-radius: 50%; background: #ffffff; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06); border: 4rpx solid #ffffff; transition: transform 0.2s; } .circle-hover { transform: scale(0.95); } .avatar-img { width: 100%; height: 100%; border-radius: 50%; } .avatar-placeholder { width: 100%; height: 100%; border-radius: 50%; background: #f2f3f5; display: flex; align-items: center; justify-content: center; } .badge-add { position: absolute; right: 0; bottom: 0; width: 44rpx; height: 44rpx; border-radius: 50%; background: #1d1d1f; display: flex; align-items: center; justify-content: center; border: 4rpx solid #ffffff; } .avatar-tip { font-size: 24rpx; color: #86868b; margin-top: 16rpx; } .form-card { background: #ffffff; border-radius: 24rpx; padding: 30rpx 40rpx; margin-bottom: 24rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02); } .card-title { font-size: 26rpx; font-weight: 600; color: #1d1d1f; margin-bottom: 24rpx; opacity: 0.8; } .form-row { display: flex; align-items: center; min-height: 90rpx; padding: 10rpx 0; } .f-input { flex: 1; height: 100%; font-size: 30rpx; color: #1d1d1f; } .f-divider { height: 1px; background: #f0f0f0; transform: scaleY(0.5); margin: 0; } .is-link { justify-content: space-between; } .f-val { font-size: 30rpx; color: #1d1d1f; flex: 1; } .is-empty { color: #b2b2b2; } .cell-hover { background-color: #f9f9f9; border-radius: 12rpx; margin: 0 -20rpx; padding: 10rpx 20rpx; } .flex-1 { flex: 1; min-width: 0; } .btn-code { font-size: 26rpx; color: #007aff; padding-left: 20rpx; border-left: 1px solid #f0f0f0; font-weight: 500; } .is-disabled { color: #b2b2b2; } .license-upload-box { height: 200rpx; background: #f8f9fa; border-radius: 16rpx; border: 2rpx dashed #dcdfe6; display: flex; align-items: center; justify-content: center; overflow: hidden; margin-top: 10rpx; } .license-img { width: 100%; height: 100%; } .license-placeholder { display: flex; flex-direction: column; align-items: center; gap: 12rpx; font-size: 26rpx; color: #86868b; } .loc-wrap { display: flex; align-items: center; gap: 12rpx; flex: 1; min-width: 0; } .ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .footer { margin-top: 60rpx; padding: 0 20rpx; } .btn-submit { width: 100%; height: 96rpx; border-radius: 48rpx; background: #1d1d1f; color: #ffffff; font-size: 32rpx; font-weight: 600; display: flex; align-items: center; justify-content: center; box-shadow: 0 12rpx 30rpx rgba(29, 29, 31, 0.2); transition: all 0.2s; } .btn-hover { opacity: 0.85; transform: scale(0.98); } .is-loading { opacity: 0.7; pointer-events: none; } </style>看一下为什么更换两个头像都失败了 返回一个完整的更换头像页面text<scroll-view scroll-y class="page-scroll" :style="{ top: headerTotalPx + 'px' }"> <view class="container"> <view class="identity-switcher"> <view class="switch-item" :class="{ 'is-active': isEnterpriseMode }" hover-class="btn-hover" @click="setIdentityMode(true)"> <uni-icons :type="isEnterpriseMode ? 'shop-filled' : 'shop'" :color="isEnterpriseMode ? '#d89a35' : '#86868b'" size="20"></uni-icons> <text>企业招人</text> </view> <view class="switch-item" :class="{ 'is-active': !isEnterpriseMode }" hover-class="btn-hover" @click="setIdentityMode(false)"> <uni-icons :type="!isEnterpriseMode ? 'person-filled' : 'person'" :color="!isEnterpriseMode ? '#007aff' : '#86868b'" size="20"></uni-icons> <text>个人求职</text> </view> </view> <view class="avatar-section"> <view class="avatar-upload" hover-class="circle-hover" @click="pickImage(isEnterpriseMode ? enterpriseAvatarFile : applicantAvatarFile)"> <image v-if="activeAvatarPreview" class="avatar-img" :src="activeAvatarPreview" mode="aspectFill" /> <view v-else class="avatar-placeholder"> <uni-icons type="camera-filled" color="#b2b2b2" size="40"></uni-icons> </view> <view class="badge-add"> <uni-icons type="plusempty" color="#fff" size="14"></uni-icons> </view> </view> <text class="avatar-tip">{{ isEnterpriseMode ? '上传企业 Logo' : '上传真实头像' }}</text> </view> <template v-if="isEnterpriseMode"> <view class="form-card"> <view class="card-title">基本信息</view> <view class="form-row"> <input v-model.trim="enterpriseForm.organization.name" class="f-input" placeholder="请输入完整企业名称" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <picker :range="industryOptions" :value="industryIndex" @change="onIndustryChange"> <view class="form-row is-link" hover-class="cell-hover"> <text class="f-val" :class="{ 'is-empty': industryIndex < 0 }"> {{ currentIndustry || '请选择所属行业' }} </text> <uni-icons type="right" color="#c0c4cc" size="16"></uni-icons> </view> </picker> <view class="f-divider"></view> <picker :range="scaleOptions" :value="scaleIndex" @change="onScaleChange"> <view class="form-row is-link" hover-class="cell-hover"> <text class="f-val" :class="{ 'is-empty': scaleIndex < 0 }"> {{ currentScale || '请选择企业规模' }} </text> <uni-icons type="right" color="#c0c4cc" size="16"></uni-icons> </view> </picker> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="enterpriseForm.organization.aboutUs" class="f-input" placeholder="一句话公司理念/简介 (选填)" placeholder-style="color:#b2b2b2;" /> </view> </view> <view class="form-card"> <view class="card-title">管理员与账户设置</view> <view class="form-row"> <input v-model.trim="enterpriseForm.personnel.name" class="f-input" placeholder="管理员称呼 (如: 张女士)" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="enterpriseForm.personnel.idCard" type="idcard" class="f-input" placeholder="管理员身份证号" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="enterpriseForm.personnel.mobilePhone" type="number" maxlength="11" class="f-input" placeholder="登录手机号码" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="verifyCode" type="number" maxlength="6" class="f-input flex-1" placeholder="短信验证码" placeholder-style="color:#b2b2b2;" /> <view class="btn-code" hover-class="btn-hover" :class="{ 'is-disabled': countdown > 0 }" @click="sendEnterpriseCode"> {{ countdown > 0 ? `${countdown}s 后重新获取` : '获取验证码' }} </view> </view> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="enterpriseForm.password" password class="f-input" placeholder="设置登录密码 (6-18位)" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="enterpriseForm.confirmPassword" password class="f-input" placeholder="请再次输入确认密码" placeholder-style="color:#b2b2b2;" /> </view> </view> <view class="form-card"> <view class="card-title">资质认证</view> <view class="form-row"> <input v-model.trim="enterpriseForm.organization.taxpayerIdentityNumber" class="f-input" placeholder="18位统一社会信用代码" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <view class="license-upload-box" hover-class="cell-hover" @click="pickImage(licenseFile)"> <image v-if="licenseFile.previewUrl" class="license-img" :src="licenseFile.previewUrl" mode="aspectFill" /> <view v-else class="license-placeholder"> <uni-icons type="image" color="#c0c4cc" size="32"></uni-icons> <text>点击上传营业执照原件</text> </view> </view> </view> <view class="form-card"> <view class="card-title">公司地址</view> <view class="form-row is-link" hover-class="cell-hover" @click="openLocation"> <view class="loc-wrap"> <uni-icons type="location-filled" color="#d89a35" size="18"></uni-icons> <text class="f-val ellipsis" :class="{ 'is-empty': !selectedLocation.address }"> {{ selectedLocation.address || '点击选择面试办公地址' }} </text> </view> <uni-icons type="right" color="#c0c4cc" size="16"></uni-icons> </view> </view> </template> <template v-else> <view class="form-card"> <view class="card-title">基本简历</view> <view class="form-row"> <input v-model.trim="applicantForm.realName" class="f-input" placeholder="您的真实姓名" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <picker :range="educationOptions" :value="educationIndex" @change="onEducationChange"> <view class="form-row is-link" hover-class="cell-hover"> <text class="f-val" :class="{ 'is-empty': educationIndex < 0 }"> {{ applicantForm.education || '请选择最高学历' }} </text> <uni-icons type="right" color="#c0c4cc" size="16"></uni-icons> </view> </picker> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="applicantForm.position" class="f-input" placeholder="期望职位 (如: 前端开发)" placeholder-style="color:#b2b2b2;" /> </view> <view class="f-divider"></view> <view class="form-row"> <input v-model.trim="applicantForm.salaryExpectation" class="f-input" placeholder="期望薪资 (如: 10k-15k)" placeholder-style="color:#b2b2b2;" /> </view> </view> <view class="form-card"> <view class="card-title">求职状态与位置</view> <picker :range="jobStateOptions" :value="jobStateIndex" @change="onJobStateChange"> <view class="form-row is-link" hover-class="cell-hover"> <text class="f-val" :class="{ 'is-empty': jobStateIndex < 0 }"> {{ currentJobState || '请选择当前求职状态' }} </text> <uni-icons type="right" color="#c0c4cc" size="16"></uni-icons> </view> </picker> <view class="f-divider"></view> <view class="form-row is-link" hover-class="cell-hover" @click="openApplicantLocation"> <view class="loc-wrap"> <uni-icons type="location-filled" color="#007aff" size="18"></uni-icons> <text class="f-val ellipsis" :class="{ 'is-empty': !applicantForm.currentAddress }"> {{ applicantForm.currentAddress || '请选择当前所在城市/位置' }} </text> </view> <uni-icons type="right" color="#c0c4cc" size="16"></uni-icons> </view> </view> </template> <view class="footer"> <view class="btn-submit" hover-class="btn-hover" :class="{ 'is-loading': submitting }" @click="submitAll"> {{ submitting ? '处理中...' : (isEnterpriseMode ? '立即认证入驻' : '开启求职之旅') }} </view> </view> <view style="height: 60rpx;"></view> </view> </scroll-view> </view>
你这个“两个头像都失败”,大概率不是页面样式问题,而是这几个接口对接点有问题。
我先直接说结论。
你在“加入咖招”页里用的是:
jsconst injectedServerUrl = inject('serverUrl', '') || inject('webProjectUrl', '') || ''
但在“更换头像”页里只用了:
jsconst injectedServerUrl = inject('webProjectUrl', '') || ''
如果项目实际注入的是 serverUrl,那更换头像页里的 injectedServerUrl 就是空字符串,uni.uploadFile 实际请求地址就会变成:
js/recrueitJobApplicantUpdateAvatarUr /organizationUpdateAvatarUrl
这通常会直接失败。
你现在是这样判断成功:
jsif (json?.result === 'success' || json?.avatarUrl || json?.uploadedFileId)
但很多后端实际返回可能是:
js{ result: "success", data: { avatarUrl: "xxx" } }
或者:
js{ success: true, data: {...} }
这样你前端就会误判失败。
虽然你调用了:
jsroleStore.hydrateFromStorage()
但如果当前身份没同步对,或者 storage 里实际没值,就会在这里拦住:
jsif (isApplicant.value && !applicantId.value) if (!isApplicant.value && !organizationId.value)
尤其你这个 store 里 identity 会根据 organizationId / applicantId 自动纠偏,页面进入时要确保角色和 id 都已读出来。
有些接口返回相对路径,比如:
jsupload/avatar/xxx.png
你直接存进 store 后,别的页面拿出来可能显示不了。应当统一转成完整地址后再回写 store。
recrueitJobApplicantUpdateAvatarUr这个名字看起来像拼写错误,但你给的接口文档里也确实是这个名字,所以前端先不要自作主张改成别的。
真正该修的是“地址注入”和“返回解析”。
下面给你一份完整可替换的更换头像页面,已经把这些问题都补上了。
vue<template> <view :class="['app-container', theme]"> <view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> <view class="header-inner"> <view class="left-box" @click="navigateBack"> <uni-icons type="left" color="#1d1d1f" size="24" /> </view> <view class="center-box"> <text class="title-text">更新头像</text> </view> <view class="right-box"></view> </view> </view> <view class="page-main" :style="{ paddingTop: headerTotalPx + 'px' }"> <view class="avatar-editor"> <view class="avatar-circle" hover-class="circle-hover" @click="openChooseDialog"> <image v-if="previewUrl" class="avatar-img" :src="previewUrl" mode="aspectFill" /> <view v-else class="avatar-placeholder"> <uni-icons type="person-filled" color="#c0c4cc" size="80" /> </view> <view class="camera-badge"> <uni-icons type="camera-filled" color="#ffffff" size="20" /> </view> </view> <text class="tip-text">点击上方区域更换照片</text> </view> <view class="bottom-action"> <view class="btn-submit" hover-class="btn-hover" @click="submitAvatar"> {{ submitting ? '安全上传中...' : '确认更新' }} </view> </view> </view> </view> </template> <script setup> import { ref, inject, computed } from 'vue' import { storeToRefs } from 'pinia' import { onLoad } from '@dcloudio/uni-app' import { useMainStore } from '@/store/index.js' import { useUserecruitmentsharingStore } from '@/store/Userecruitmentsharing.js' import ajax from '@/api/ajax.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' const mainStore = useMainStore() const roleStore = useUserecruitmentsharingStore() const { theme } = storeToRefs(mainStore) const { isApplicant, organizationId, applicantId, organizationAvatarUrl, applicantAvatarUrl, identity } = storeToRefs(roleStore) /** * 这里一定要和“加入咖招”页面统一 * 优先 serverUrl,其次 webProjectUrl */ const injectedServerUrl = inject('serverUrl', '') || inject('webProjectUrl', '') || '' const statusBarHeight = ref( Number(getStatusBarHeight?.() || uni.getSystemInfoSync()?.statusBarHeight || 0) ) const headerBarHeight = ref(Number(getTitleBarHeight?.() || 44)) const headerTotalPx = computed(() => Number(statusBarHeight.value || 0) + Number(headerBarHeight.value || 0)) const previewUrl = ref('') const tempFilePath = ref('') const submitting = ref(false) function toast(title) { uni.showToast({ title: String(title || ''), icon: 'none' }) } function navigateBack() { uni.navigateBack({ fail: () => { const pages = getCurrentPages() if (!pages.length) { uni.switchTab({ url: '/pages/sociAlize/Coffeemu/CoffeemuHome' }) } } }) } function safeDecode(v) { let s = String(v ?? '') for (let i = 0; i < 2; i++) { if (!/%[0-9A-Fa-f]{2}/.test(s)) break try { s = decodeURIComponent(s) } catch (e) { break } } return s } function normalizeBaseUrl(url) { return String(url || '').replace(/\/$/, '') } function buildFileUrl(path) { if (!path) return '' if (/^https?:\/\//i.test(path)) return path const base = normalizeBaseUrl(injectedServerUrl) if (!base) return path return `${base}/${String(path).replace(/^\//, '')}` } function getApiData(res) { const root = res?.data ?? res ?? {} return root?.data && typeof root.data === 'object' ? root.data : root } /** * 统一兼容接口返回: * 1. { avatarUrl: '' } * 2. { data: { avatarUrl: '' } } * 3. { result: 'success', data: {...} } */ function parseUploadResponse(raw) { let json = raw if (typeof raw === 'string') { try { json = JSON.parse(raw) } catch (e) { return { ok: false, message: '返回数据解析失败', raw } } } const data = getApiData(json) const avatarUrl = data?.avatarUrl || json?.avatarUrl || '' const uploadedFileId = data?.uploadedFileId || json?.uploadedFileId || '' const result = String(data?.result || json?.result || '').toLowerCase() const ok = !!avatarUrl || !!uploadedFileId || result === 'success' return { ok, data, json, avatarUrl, uploadedFileId, message: data?.message || json?.message || (ok ? 'success' : '上传失败') } } function openChooseDialog() { uni.showActionSheet({ itemList: ['拍照', '从手机相册选择'], itemColor: '#1d1d1f', success: (res) => { const sourceType = Number(res.tapIndex) === 0 ? ['camera'] : ['album'] chooseImage(sourceType) } }) } function chooseImage(sourceType = ['album']) { uni.chooseImage({ count: 1, sourceType, sizeType: ['compressed', 'original'], success: (res) => { const path = res.tempFilePaths?.[0] if (path) { tempFilePath.value = path previewUrl.value = path } }, fail: (err) => { console.log('chooseImage fail', err) } }) } function getEventChannelSafely() { try { if (typeof getOpenerEventChannel === 'function') { return getOpenerEventChannel() } return null } catch (e) { return null } } function getUploadConfig() { if (isApplicant.value) { return { url: 'recrueitJobApplicantUpdateAvatarUr', formData: { recrueitJobApplicantId: String(applicantId.value || '') }, idLabel: '求职者' } } return { url: 'organizationUpdateAvatarUrl', formData: { organizationId: String(organizationId.value || '') }, idLabel: '机构' } } function uploadAvatarFile(filePath) { return new Promise((resolve, reject) => { const base = normalizeBaseUrl(injectedServerUrl) if (!base) { reject(new Error('未获取到服务地址 serverUrl/webProjectUrl')) return } const config = getUploadConfig() const uploadUrl = `${base}/${config.url}` console.log('uploadAvatarFile url =>', uploadUrl) console.log('uploadAvatarFile formData =>', config.formData) uni.uploadFile({ url: uploadUrl, filePath, name: 'avatarFile', formData: config.formData, header: ajax?.getHeaders?.() || {}, success: (res) => { console.log('uploadAvatarFile success =>', res) const parsed = parseUploadResponse(res?.data) if (parsed.ok) { resolve(parsed) } else { reject(new Error(parsed.message || '上传失败')) } }, fail: (err) => { console.log('uploadAvatarFile fail =>', err) reject(err) } }) }) } async function submitAvatar() { if (submitting.value) return roleStore.hydrateFromStorage() if (isApplicant.value && !applicantId.value) { toast('缺少求职者信息,请重新进入后再试') return } if (!isApplicant.value && !organizationId.value) { toast('缺少机构信息,请重新进入后再试') return } if (!tempFilePath.value) { openChooseDialog() return } submitting.value = true uni.showLoading({ title: '安全上传中...', mask: true }) try { const result = await uploadAvatarFile(tempFilePath.value) uni.hideLoading() const avatarPath = result?.avatarUrl || '' const newAvatar = buildFileUrl(avatarPath || previewUrl.value) if (isApplicant.value) { roleStore.updateApplicantAvatar(newAvatar) } else { roleStore.updateOrganizationAvatar(newAvatar) } roleStore.saveToStorage() const eventChannel = getEventChannelSafely() eventChannel?.emit('avatarUpdated', { avatarUrl: newAvatar, identity: identity.value }) uni.showToast({ title: '头像已更新', icon: 'success' }) setTimeout(() => { uni.navigateBack() }, 800) } catch (e) { uni.hideLoading() console.log('submitAvatar error =>', e) toast(String(e?.message || '上传失败,请重试')) } finally { submitting.value = false } } onLoad((option) => { roleStore.hydrateFromStorage() const currentRawAvatar = safeDecode( option?.avatarUrl || (isApplicant.value ? applicantAvatarUrl.value : organizationAvatarUrl.value) || '' ) previewUrl.value = buildFileUrl(currentRawAvatar) console.log('avatar page onLoad =>', { injectedServerUrl, identity: identity.value, isApplicant: isApplicant.value, applicantId: applicantId.value, organizationId: organizationId.value, currentRawAvatar, previewUrl: previewUrl.value }) }) </script> <style lang="scss" scoped> .app-container { min-height: 100vh; background: #f7f8fa; } .header { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(247, 248, 250, 0.9); backdrop-filter: blur(10px); } .header-inner { display: flex; align-items: center; justify-content: space-between; height: 100%; padding: 0 30rpx; } .left-box, .right-box { width: 80rpx; display: flex; align-items: center; } .left-box { justify-content: flex-start; } .right-box { justify-content: flex-end; } .center-box { flex: 1; display: flex; justify-content: center; align-items: center; } .title-text { font-size: 34rpx; font-weight: 600; color: #1d1d1f; } .page-main { display: flex; flex-direction: column; align-items: center; min-height: 100vh; box-sizing: border-box; } .avatar-editor { margin-top: 140rpx; display: flex; flex-direction: column; align-items: center; } .avatar-circle { position: relative; width: 300rpx; height: 300rpx; border-radius: 50%; background: #ffffff; box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.08); display: flex; align-items: center; justify-content: center; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; } .circle-hover { transform: scale(0.94); } .avatar-img { width: 100%; height: 100%; border-radius: 50%; border: 6rpx solid #ffffff; box-sizing: border-box; } .avatar-placeholder { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; } .camera-badge { position: absolute; right: 10rpx; bottom: 16rpx; width: 72rpx; height: 72rpx; border-radius: 50%; background: #1d1d1f; border: 6rpx solid #ffffff; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2); z-index: 2; } .tip-text { margin-top: 50rpx; font-size: 28rpx; color: #86868b; } .bottom-action { margin-top: auto; margin-bottom: 120rpx; width: 85%; } .btn-submit { width: 100%; height: 96rpx; border-radius: 48rpx; background: #1d1d1f; color: #ffffff; font-size: 32rpx; font-weight: 600; display: flex; align-items: center; justify-content: center; box-shadow: 0 12rpx 30rpx rgba(29, 29, 31, 0.2); transition: all 0.2s; } .btn-hover { opacity: 0.85; transform: scale(0.98); } </style>
这版已经把关键问题都补上了:
serverUrl 和 webProjectUrlconsole.log,方便你真机排查你项目入口如果是:
jsapp.provide('serverUrl', xxx)
那旧版更换头像页一定会失败。
你可以重点看这里的日志:
jsconsole.log('uploadAvatarFile success =>', res)
如果后端返回的是:
js{ data: { avatarUrl: 'xxx' } }
那旧代码就会误判失败,新代码已经兼容了。
如果 ajax.getHeaders() 里没有登录态,而上传接口又要求登录,也会失败。
你可以打印:
jsconsole.log(ajax?.getHeaders?.())
进入页面时你会看到日志:
jsconsole.log('avatar page onLoad =>', { injectedServerUrl, identity, isApplicant, applicantId, organizationId })
如果这里 id 为空,就不是上传问题,是前面身份缓存没存进去。
如果你现在不想整页替换,最少先改这两处:
jsconst injectedServerUrl = inject('webProjectUrl', '') || ''
改成:
jsconst injectedServerUrl = inject('serverUrl', '') || inject('webProjectUrl', '') || ''
jsconst json = typeof res.data === 'string' ? JSON.parse(res.data) : res.data const body = json?.data && typeof json.data === 'object' ? json.data : json if (json?.result === 'success' || body?.avatarUrl || body?.uploadedFileId) { resolve({ ...json, ...body }) return }
如果你愿意,我下一条可以继续直接把“咖募聊天页 + 头像实时刷新 + websocket 收到头像更新后的会话列表同步”也一起补成完整可用版。