You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
945 lines
26 KiB
945 lines
26 KiB
/**
|
|
* 企业端 · 投递详情
|
|
*
|
|
* 这是企业侧投递链路的第二层:在这一页完成两件**会改变数据**的事 ——
|
|
* ① 推进投递状态(后端只接受正向流转)
|
|
* ② 写 / 改 / 删候选人反馈(一条投递最多一条反馈)
|
|
*
|
|
* 页面本身不做业务判断:下一步能做什么由 constants 的 APPLICATION_STATUS_TRANSITIONS 决定,
|
|
* 「已有反馈该走 PUT 还是 POST」由 hook 决定。页面只负责渲染与二次确认。
|
|
*/
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Modal,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { RatingStars, STAR_COLOR } from '../components/RatingStars';
|
|
import { ErrorState, LoadingState } from '../src/components/StateView';
|
|
import {
|
|
APPLICATION_STATUS_LABELS,
|
|
APPLICATION_STATUS_TRANSITIONS,
|
|
CANDIDATE_FEEDBACK_TAG_SUGGESTIONS,
|
|
} from '../src/constants';
|
|
import { useCompanyApplicationDetail } from '../src/hooks';
|
|
import { applicationStatusColors, colors, spacing } from '../src/theme';
|
|
import type {
|
|
ApplicationStatus,
|
|
CandidateFeedback,
|
|
CandidateFeedbackInput,
|
|
CompanyCandidateDetail,
|
|
} from '../src/types';
|
|
|
|
/** 反馈编辑器初值;编辑已有反馈时回填 */
|
|
const EMPTY_FEEDBACK: CandidateFeedbackInput = { rating: 0, content: '', tags: [] };
|
|
|
|
function formatDateTime(iso?: string): string {
|
|
if (!iso) return '—';
|
|
const date = new Date(iso);
|
|
if (Number.isNaN(date.getTime())) return '—';
|
|
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
|
|
date.getHours(),
|
|
)}:${pad(date.getMinutes())}`;
|
|
}
|
|
|
|
/* ===================== 子组件 ===================== */
|
|
|
|
function StatusBadge({ status }: { status: ApplicationStatus }) {
|
|
const tone = applicationStatusColors[status];
|
|
return (
|
|
<View style={[styles.badge, { backgroundColor: tone.bg }]}>
|
|
<Text style={[styles.badgeText, { color: tone.color }]}>
|
|
{APPLICATION_STATUS_LABELS[status]}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SectionTitle({ title }: { title: string }) {
|
|
return <Text style={styles.sectionTitle}>{title}</Text>;
|
|
}
|
|
|
|
/** 候选人 + 岗位信息 */
|
|
function ProfileCard({ detail }: { detail: CompanyCandidateDetail }) {
|
|
const meta = [
|
|
detail.candidateSchool,
|
|
detail.candidateEducation,
|
|
detail.candidateGraduationYear ? `${detail.candidateGraduationYear} 届` : undefined,
|
|
detail.candidateLocation,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
|
|
return (
|
|
<View style={styles.card}>
|
|
<View style={styles.profileTop}>
|
|
<View style={styles.avatar}>
|
|
<Text style={styles.avatarText}>{detail.candidateName.slice(0, 1)}</Text>
|
|
</View>
|
|
<View style={styles.profileMain}>
|
|
<View style={styles.profileNameRow}>
|
|
<Text style={styles.name}>{detail.candidateName}</Text>
|
|
<StatusBadge status={detail.status} />
|
|
</View>
|
|
<Text style={styles.meta}>{meta || '学历信息未公开'}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{detail.candidateBio ? (
|
|
<Text style={styles.bio}>{detail.candidateBio}</Text>
|
|
) : null}
|
|
|
|
{detail.candidateSkills && detail.candidateSkills.length > 0 ? (
|
|
<View style={styles.chips}>
|
|
{detail.candidateSkills.map((skill) => (
|
|
<View key={skill} style={styles.chip}>
|
|
<Text style={styles.chipText}>{skill}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
|
|
<View style={styles.divider} />
|
|
|
|
<View style={styles.kvRow}>
|
|
<Text style={styles.kvLabel}>应聘岗位</Text>
|
|
<Text style={styles.kvValue}>{detail.jobTitle}</Text>
|
|
</View>
|
|
<View style={styles.kvRow}>
|
|
<Text style={styles.kvLabel}>用工类型</Text>
|
|
<Text style={styles.kvValue}>
|
|
{[detail.jobEmploymentType, detail.jobCity].filter(Boolean).join(' · ') || '—'}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.kvRow}>
|
|
<Text style={styles.kvLabel}>投递时间</Text>
|
|
<Text style={styles.kvValue}>{formatDateTime(detail.createdAt)}</Text>
|
|
</View>
|
|
<View style={styles.kvRow}>
|
|
<Text style={styles.kvLabel}>最近更新</Text>
|
|
<Text style={styles.kvValue}>{formatDateTime(detail.updatedAt)}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** 状态流转:只渲染允许的下一步,终态给出明确说明而不是留白 */
|
|
function StatusActionCard({
|
|
status,
|
|
updating,
|
|
onChange,
|
|
}: {
|
|
status: ApplicationStatus;
|
|
updating: boolean;
|
|
onChange: (next: ApplicationStatus) => void;
|
|
}) {
|
|
const nextOptions = APPLICATION_STATUS_TRANSITIONS[status];
|
|
return (
|
|
<View style={styles.card}>
|
|
<View style={styles.statusRow}>
|
|
<Text style={styles.statusLabel}>当前状态</Text>
|
|
<StatusBadge status={status} />
|
|
</View>
|
|
{nextOptions.length === 0 ? (
|
|
<Text style={styles.terminalHint}>
|
|
{status === 'hired'
|
|
? '该候选人已录用,投递流程结束。'
|
|
: status === 'rejected'
|
|
? '该投递已拒绝,流程结束。'
|
|
: '该投递已结束,无法再变更状态。'}
|
|
</Text>
|
|
) : (
|
|
<>
|
|
<Text style={styles.actionHint}>推进到:</Text>
|
|
<View style={styles.actionRow}>
|
|
{nextOptions.map((next) => {
|
|
const tone = applicationStatusColors[next];
|
|
const isReject = next === 'rejected';
|
|
return (
|
|
<Pressable
|
|
key={next}
|
|
style={[
|
|
styles.actionBtn,
|
|
{ backgroundColor: tone.bg },
|
|
updating && styles.actionBtnDisabled,
|
|
]}
|
|
disabled={updating}
|
|
onPress={() => onChange(next)}
|
|
>
|
|
{updating ? (
|
|
<ActivityIndicator size="small" color={tone.color} />
|
|
) : (
|
|
<Text
|
|
style={[
|
|
styles.actionText,
|
|
{ color: tone.color },
|
|
isReject && styles.actionTextDanger,
|
|
]}
|
|
>
|
|
{APPLICATION_STATUS_LABELS[next]}
|
|
</Text>
|
|
)}
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** 候选人反馈区:已有反馈可改可删,没有则引导去写 */
|
|
function FeedbackCard({
|
|
feedback,
|
|
onEdit,
|
|
onDelete,
|
|
busy,
|
|
}: {
|
|
feedback: CandidateFeedback | null;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
busy: boolean;
|
|
}) {
|
|
if (!feedback) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<View style={styles.feedbackEmpty}>
|
|
<Ionicons name="chatbox-ellipses-outline" size={28} color={colors.textHint} />
|
|
<Text style={styles.feedbackEmptyTitle}>还没有写反馈</Text>
|
|
<Text style={styles.feedbackEmptyHint}>
|
|
记录这位候选人的面试表现,后续 Offer 决策时可回溯
|
|
</Text>
|
|
<Pressable style={styles.primaryBtn} onPress={onEdit}>
|
|
<Text style={styles.primaryBtnText}>写反馈</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.card}>
|
|
<View style={styles.feedbackTop}>
|
|
<RatingStars value={feedback.rating} size={15} />
|
|
<Text style={styles.feedbackRating}>{feedback.rating}.0</Text>
|
|
</View>
|
|
<Text style={styles.feedbackContent}>{feedback.content}</Text>
|
|
{feedback.tags.length > 0 ? (
|
|
<View style={styles.chips}>
|
|
{feedback.tags.map((tag) => (
|
|
<View key={tag} style={styles.chip}>
|
|
<Text style={styles.chipText}>{tag}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
<Text style={styles.feedbackTime}>最后更新 {formatDateTime(feedback.updatedAt)}</Text>
|
|
<View style={styles.feedbackActions}>
|
|
<Pressable style={styles.linkBtn} onPress={onEdit} disabled={busy}>
|
|
<Ionicons name="create-outline" size={14} color={colors.primaryText} />
|
|
<Text style={styles.linkText}>修改</Text>
|
|
</Pressable>
|
|
<Pressable style={styles.linkBtn} onPress={onDelete} disabled={busy}>
|
|
{busy ? (
|
|
<ActivityIndicator size="small" color={colors.dangerText} />
|
|
) : (
|
|
<>
|
|
<Ionicons name="trash-outline" size={14} color={colors.dangerText} />
|
|
<Text style={[styles.linkText, styles.linkTextDanger]}>删除</Text>
|
|
</>
|
|
)}
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/** 反馈编辑器(新建 / 修改共用) */
|
|
function FeedbackEditorModal({
|
|
visible,
|
|
initial,
|
|
submitting,
|
|
onClose,
|
|
onSubmit,
|
|
}: {
|
|
visible: boolean;
|
|
initial: CandidateFeedback | null;
|
|
submitting: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (input: CandidateFeedbackInput) => void;
|
|
}) {
|
|
const [draft, setDraft] = useState<CandidateFeedbackInput>(EMPTY_FEEDBACK);
|
|
const [seededFor, setSeededFor] = useState<string | null>(null);
|
|
|
|
// 打开时按 initial 播种一次:用「是否已播种过这条反馈」判断,避免每次父组件重渲染都覆盖用户正在输入的内容
|
|
const seedKey = visible ? (initial?.id ?? 'new') : null;
|
|
if (seedKey !== seededFor) {
|
|
setSeededFor(seedKey);
|
|
setDraft(
|
|
initial
|
|
? { rating: initial.rating, content: initial.content, tags: [...initial.tags] }
|
|
: EMPTY_FEEDBACK,
|
|
);
|
|
}
|
|
|
|
const toggleTag = (tag: string) => {
|
|
setDraft((prev) => ({
|
|
...prev,
|
|
tags: prev.tags.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...prev.tags, tag],
|
|
}));
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (draft.rating <= 0) {
|
|
Alert.alert('提示', '请先给候选人一个评分');
|
|
return;
|
|
}
|
|
if (!draft.content.trim()) {
|
|
Alert.alert('提示', '请填写反馈内容');
|
|
return;
|
|
}
|
|
onSubmit({ ...draft, content: draft.content.trim() });
|
|
};
|
|
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
<KeyboardAvoidingView
|
|
style={styles.modalMask}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<View style={styles.modalSheet}>
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>{initial ? '修改反馈' : '写反馈'}</Text>
|
|
<Pressable onPress={onClose} hitSlop={8} accessibilityLabel="关闭">
|
|
<Ionicons name="close" size={22} color={colors.textSecondary} />
|
|
</Pressable>
|
|
</View>
|
|
<ScrollView style={styles.modalBody} keyboardShouldPersistTaps="handled">
|
|
<Text style={styles.fieldLabel}>评分</Text>
|
|
<View style={styles.ratingRow}>
|
|
<RatingStars
|
|
value={draft.rating}
|
|
size={26}
|
|
onChange={(next) => setDraft((prev) => ({ ...prev, rating: next }))}
|
|
/>
|
|
<Text style={styles.ratingHint}>
|
|
{draft.rating > 0 ? `${draft.rating} 分` : '点击星星评分'}
|
|
</Text>
|
|
</View>
|
|
|
|
<Text style={styles.fieldLabel}>标签(选填,可多选)</Text>
|
|
<View style={styles.tagPicker}>
|
|
{CANDIDATE_FEEDBACK_TAG_SUGGESTIONS.map((tag) => {
|
|
const active = draft.tags.includes(tag);
|
|
return (
|
|
<Pressable
|
|
key={tag}
|
|
style={[styles.tagChip, active && styles.tagChipActive]}
|
|
onPress={() => toggleTag(tag)}
|
|
>
|
|
<Text style={[styles.tagChipText, active && styles.tagChipTextActive]}>
|
|
{tag}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
<Text style={styles.fieldLabel}>反馈内容</Text>
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
placeholder="记录面试表现、亮点与顾虑,供后续决策回溯..."
|
|
placeholderTextColor={colors.textHint}
|
|
value={draft.content}
|
|
onChangeText={(text) => setDraft((prev) => ({ ...prev, content: text }))}
|
|
multiline
|
|
maxLength={500}
|
|
/>
|
|
<Text style={styles.counterText}>{draft.content.length}/500</Text>
|
|
</ScrollView>
|
|
|
|
<View style={styles.modalFooter}>
|
|
<Pressable
|
|
style={[styles.submitBtn, submitting && styles.submitBtnDisabled]}
|
|
onPress={handleSubmit}
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? (
|
|
<ActivityIndicator size="small" color={colors.white} />
|
|
) : (
|
|
<Text style={styles.submitText}>{initial ? '保存修改' : '提交反馈'}</Text>
|
|
)}
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
/* ===================== 页面 ===================== */
|
|
|
|
export default function ApplicationDetailScreen() {
|
|
const router = useRouter();
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const applicationId = id ?? '';
|
|
|
|
const {
|
|
detail,
|
|
timeline,
|
|
feedback,
|
|
loading,
|
|
error,
|
|
refresh,
|
|
updating,
|
|
savingFeedback,
|
|
changeStatus,
|
|
saveFeedback,
|
|
removeFeedback,
|
|
} = useCompanyApplicationDetail(applicationId);
|
|
|
|
const [editorOpen, setEditorOpen] = useState(false);
|
|
|
|
const handleChangeStatus = (next: ApplicationStatus) => {
|
|
Alert.alert(
|
|
'确认变更状态',
|
|
`将投递状态改为「${APPLICATION_STATUS_LABELS[next]}」?该操作会同步给学生端。`,
|
|
[
|
|
{ text: '取消', style: 'cancel' },
|
|
{
|
|
text: '确认变更',
|
|
style: next === 'rejected' ? 'destructive' : 'default',
|
|
onPress: async () => {
|
|
try {
|
|
await changeStatus(next);
|
|
} catch (e) {
|
|
Alert.alert('变更失败', (e as Error).message || '请稍后重试');
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
};
|
|
|
|
const handleSubmitFeedback = async (input: CandidateFeedbackInput) => {
|
|
try {
|
|
await saveFeedback(input, feedback);
|
|
setEditorOpen(false);
|
|
} catch (e) {
|
|
Alert.alert('保存失败', (e as Error).message || '请稍后重试');
|
|
}
|
|
};
|
|
|
|
const handleDeleteFeedback = () => {
|
|
Alert.alert('删除反馈', '删除后无法恢复,确定要删除这条反馈吗?', [
|
|
{ text: '取消', style: 'cancel' },
|
|
{
|
|
text: '删除',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
try {
|
|
await removeFeedback();
|
|
} catch (e) {
|
|
Alert.alert('删除失败', (e as Error).message || '请稍后重试');
|
|
}
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
|
|
<View style={styles.header}>
|
|
<Pressable style={styles.headerBtn} onPress={() => router.back()} hitSlop={8}>
|
|
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
|
</Pressable>
|
|
<Text style={styles.headerTitle}>投递详情</Text>
|
|
<View style={styles.headerBtn} />
|
|
</View>
|
|
|
|
{loading ? (
|
|
<LoadingState text="正在加载投递详情…" />
|
|
) : error || !detail ? (
|
|
<ErrorState message="投递详情加载失败" onRetry={refresh} />
|
|
) : (
|
|
<ScrollView
|
|
style={styles.scroll}
|
|
contentContainerStyle={styles.content}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<ProfileCard detail={detail} />
|
|
|
|
<SectionTitle title="投递状态" />
|
|
<StatusActionCard
|
|
status={detail.status}
|
|
updating={updating}
|
|
onChange={handleChangeStatus}
|
|
/>
|
|
|
|
{detail.coverLetter ? (
|
|
<>
|
|
<SectionTitle title="求职信" />
|
|
<View style={styles.card}>
|
|
<Text style={styles.coverLetter}>{detail.coverLetter}</Text>
|
|
</View>
|
|
</>
|
|
) : null}
|
|
|
|
<SectionTitle title="状态时间线" />
|
|
{timeline.length === 0 ? (
|
|
<View style={styles.card}>
|
|
<Text style={styles.timelineEmpty}>暂无状态变更记录</Text>
|
|
</View>
|
|
) : (
|
|
<View style={styles.card}>
|
|
{timeline.map((event, index) => (
|
|
<View key={`${event.status}-${index}`} style={styles.timelineRow}>
|
|
<View style={styles.timelineDot} />
|
|
<View style={styles.timelineMain}>
|
|
<Text style={styles.timelineStatus}>
|
|
{APPLICATION_STATUS_LABELS[event.status] ?? event.status}
|
|
</Text>
|
|
{event.note ? (
|
|
<Text style={styles.timelineNote}>{event.note}</Text>
|
|
) : null}
|
|
</View>
|
|
<Text style={styles.timelineTime}>{formatDateTime(event.time)}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
<SectionTitle title="候选人反馈" />
|
|
<FeedbackCard
|
|
feedback={feedback}
|
|
busy={savingFeedback}
|
|
onEdit={() => setEditorOpen(true)}
|
|
onDelete={handleDeleteFeedback}
|
|
/>
|
|
</ScrollView>
|
|
)}
|
|
|
|
<FeedbackEditorModal
|
|
visible={editorOpen}
|
|
initial={feedback}
|
|
submitting={savingFeedback}
|
|
onClose={() => setEditorOpen(false)}
|
|
onSubmit={handleSubmitFeedback}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safeArea: {
|
|
flex: 1,
|
|
backgroundColor: colors.pageBg,
|
|
},
|
|
header: {
|
|
height: 48,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
headerBtn: {
|
|
width: 48,
|
|
height: 48,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
headerTitle: {
|
|
flex: 1,
|
|
textAlign: 'center',
|
|
fontSize: 17,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
},
|
|
scroll: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
paddingHorizontal: spacing.framework,
|
|
paddingBottom: 24,
|
|
},
|
|
sectionTitle: {
|
|
marginTop: spacing.card,
|
|
marginBottom: spacing.card,
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
},
|
|
/* ---- 卡片 ---- */
|
|
card: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: spacing.card,
|
|
padding: spacing.card + 4,
|
|
marginBottom: spacing.card,
|
|
},
|
|
badge: {
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 2,
|
|
borderRadius: spacing.card,
|
|
},
|
|
badgeText: {
|
|
fontSize: 11,
|
|
fontWeight: '600',
|
|
},
|
|
/* ---- 候选人 / 岗位 ---- */
|
|
profileTop: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing.card,
|
|
},
|
|
avatar: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 24,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
avatarText: {
|
|
fontSize: 20,
|
|
fontWeight: '700',
|
|
color: colors.primaryText,
|
|
},
|
|
profileMain: {
|
|
flex: 1,
|
|
},
|
|
profileNameRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing.card,
|
|
},
|
|
name: {
|
|
fontSize: 17,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
},
|
|
meta: {
|
|
marginTop: 4,
|
|
fontSize: 12,
|
|
color: colors.textSecondary,
|
|
},
|
|
bio: {
|
|
marginTop: spacing.card + 4,
|
|
fontSize: 13,
|
|
lineHeight: 20,
|
|
color: colors.textPrimary,
|
|
},
|
|
chips: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
marginTop: spacing.card + 2,
|
|
gap: spacing.card,
|
|
},
|
|
chip: {
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 3,
|
|
borderRadius: spacing.card,
|
|
backgroundColor: colors.pageBg,
|
|
},
|
|
chipText: {
|
|
fontSize: 11,
|
|
color: colors.textSecondary,
|
|
},
|
|
divider: {
|
|
height: 1,
|
|
marginVertical: spacing.card + 4,
|
|
backgroundColor: colors.borderLight,
|
|
},
|
|
kvRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
paddingVertical: 4,
|
|
},
|
|
kvLabel: {
|
|
fontSize: 12,
|
|
color: colors.textHint,
|
|
},
|
|
kvValue: {
|
|
flexShrink: 1,
|
|
textAlign: 'right',
|
|
fontSize: 12,
|
|
color: colors.textPrimary,
|
|
},
|
|
/* ---- 状态流转 ---- */
|
|
statusRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
},
|
|
statusLabel: {
|
|
fontSize: 13,
|
|
color: colors.textSecondary,
|
|
},
|
|
terminalHint: {
|
|
marginTop: spacing.card + 4,
|
|
fontSize: 12,
|
|
color: colors.textHint,
|
|
},
|
|
actionHint: {
|
|
marginTop: spacing.card + 4,
|
|
marginBottom: spacing.card,
|
|
fontSize: 12,
|
|
color: colors.textHint,
|
|
},
|
|
actionRow: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: spacing.card,
|
|
},
|
|
actionBtn: {
|
|
minWidth: 84,
|
|
height: 34,
|
|
paddingHorizontal: 12,
|
|
borderRadius: spacing.card,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
actionBtnDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
actionText: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
},
|
|
actionTextDanger: {
|
|
fontWeight: '700',
|
|
},
|
|
/* ---- 求职信 / 时间线 ---- */
|
|
coverLetter: {
|
|
fontSize: 13,
|
|
lineHeight: 20,
|
|
color: colors.textPrimary,
|
|
},
|
|
timelineEmpty: {
|
|
fontSize: 12,
|
|
color: colors.textHint,
|
|
},
|
|
timelineRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
paddingVertical: 5,
|
|
},
|
|
timelineDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
marginTop: 5,
|
|
marginRight: spacing.card,
|
|
backgroundColor: colors.primary,
|
|
},
|
|
timelineMain: {
|
|
flex: 1,
|
|
},
|
|
timelineStatus: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
color: colors.textPrimary,
|
|
},
|
|
timelineNote: {
|
|
marginTop: 2,
|
|
fontSize: 11,
|
|
color: colors.textSecondary,
|
|
},
|
|
timelineTime: {
|
|
fontSize: 11,
|
|
color: colors.textHint,
|
|
},
|
|
/* ---- 反馈 ---- */
|
|
feedbackEmpty: {
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
paddingVertical: spacing.card,
|
|
},
|
|
feedbackEmptyTitle: {
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
color: colors.textSecondary,
|
|
},
|
|
feedbackEmptyHint: {
|
|
fontSize: 12,
|
|
color: colors.textHint,
|
|
textAlign: 'center',
|
|
},
|
|
primaryBtn: {
|
|
marginTop: spacing.card,
|
|
paddingHorizontal: 24,
|
|
height: 36,
|
|
borderRadius: spacing.card,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: colors.primaryBtn,
|
|
},
|
|
primaryBtnText: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
color: colors.white,
|
|
},
|
|
feedbackTop: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
},
|
|
feedbackRating: {
|
|
fontSize: 13,
|
|
fontWeight: '700',
|
|
color: STAR_COLOR,
|
|
},
|
|
feedbackContent: {
|
|
marginTop: spacing.card,
|
|
fontSize: 13,
|
|
lineHeight: 20,
|
|
color: colors.textPrimary,
|
|
},
|
|
feedbackTime: {
|
|
marginTop: spacing.card,
|
|
fontSize: 11,
|
|
color: colors.textHint,
|
|
},
|
|
feedbackActions: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
marginTop: spacing.card,
|
|
paddingTop: spacing.card,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
gap: 16,
|
|
},
|
|
linkBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
linkText: {
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
color: colors.primaryText,
|
|
},
|
|
linkTextDanger: {
|
|
color: colors.dangerText,
|
|
},
|
|
/* ---- 编辑器弹窗 ---- */
|
|
modalMask: {
|
|
flex: 1,
|
|
backgroundColor: colors.scrim,
|
|
justifyContent: 'flex-end',
|
|
},
|
|
modalSheet: {
|
|
maxHeight: '86%',
|
|
backgroundColor: colors.white,
|
|
borderTopLeftRadius: spacing.framework,
|
|
borderTopRightRadius: spacing.framework,
|
|
},
|
|
modalHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: spacing.framework,
|
|
paddingVertical: 14,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.borderLight,
|
|
},
|
|
modalTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: colors.textPrimary,
|
|
},
|
|
modalBody: {
|
|
paddingHorizontal: spacing.framework,
|
|
paddingTop: 12,
|
|
},
|
|
fieldLabel: {
|
|
marginTop: 14,
|
|
marginBottom: 8,
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
color: colors.textPrimary,
|
|
},
|
|
ratingRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
ratingHint: {
|
|
fontSize: 13,
|
|
color: colors.textSecondary,
|
|
},
|
|
tagPicker: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: spacing.card,
|
|
},
|
|
tagChip: {
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 5,
|
|
borderRadius: spacing.card,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.white,
|
|
},
|
|
tagChipActive: {
|
|
borderColor: colors.primary,
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
tagChipText: {
|
|
fontSize: 12,
|
|
color: colors.textSecondary,
|
|
},
|
|
tagChipTextActive: {
|
|
color: colors.primaryText,
|
|
fontWeight: '600',
|
|
},
|
|
modalInput: {
|
|
minHeight: 96,
|
|
borderRadius: spacing.card,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.pageBg,
|
|
paddingHorizontal: spacing.card,
|
|
paddingVertical: spacing.card,
|
|
fontSize: 13,
|
|
lineHeight: 20,
|
|
color: colors.textPrimary,
|
|
textAlignVertical: 'top',
|
|
},
|
|
counterText: {
|
|
marginTop: 6,
|
|
textAlign: 'right',
|
|
fontSize: 11,
|
|
color: colors.textHint,
|
|
},
|
|
modalFooter: {
|
|
paddingHorizontal: spacing.framework,
|
|
paddingTop: 10,
|
|
paddingBottom: 24,
|
|
},
|
|
submitBtn: {
|
|
height: 44,
|
|
borderRadius: 22,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: colors.primaryBtn,
|
|
},
|
|
submitBtnDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
submitText: {
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
color: colors.white,
|
|
},
|
|
});
|
|
|