diff --git a/src/views/workplace-collaboration-view.jsx b/src/views/workplace-collaboration-view.jsx index ba8c5278..e3c23b4c 100644 --- a/src/views/workplace-collaboration-view.jsx +++ b/src/views/workplace-collaboration-view.jsx @@ -695,7 +695,7 @@ function DailyReportCard({report, names, team}) { {STATUS_LABEL[report.status] || report.status}

{report.content_short || '(未填写内容)'}

+ ; } @@ -768,6 +768,11 @@ function DailyReportList({team, myUserId, onToast}) { function DailyReportDetail({hashParams, myUserId, onToast}) { const reportId = hashParams && hashParams.report_id; + // DailyReportCard renders identically for DailyReportList's team=true (团队统计) and team=false + // (我的日报) modes and always links here -- backTarget hardcoded to daily-report-mine used to send + // a manager who'd been browsing 团队统计 back to their own report list instead of the team roster + // they'd actually come from. + const backTarget = hashParams && hashParams.team ? 'daily-report-team' : 'daily-report-mine'; const [report, setReport] = React.useState(() => (reportId && readCachedData('daily-report:' + reportId)) || null); const [loadError, setLoadError] = React.useState(false); const names = useUserNames(report ? [report.author_id] : [], myUserId); @@ -779,9 +784,9 @@ function DailyReportDetail({hashParams, myUserId, onToast}) { setReport(readCachedData('daily-report:' + reportId) || null); getDailyReport(reportId).then(r => { writeCachedData('daily-report:' + reportId, r); setReport(r); }).catch(err => { onToast(err.message || '日报加载失败'); setLoadError(true); }); }, [reportId]); - if (!report) return

{loadError ? '日报加载失败' : '加载中…'}

; + if (!report) return

{loadError ? '日报加载失败' : '加载中…'}

; - return
+ return
{names[report.author_id] || '…'}员工 {STATUS_LABEL[report.status] || report.status}
@@ -953,7 +958,7 @@ function MonthlyReportList({team, myUserId, onToast}) { {reports == null ?

加载中…

: filtered.length === 0 ?

{loadError ? '月报列表加载失败' : '暂无月报'}

: filtered.map(r => \n

{title}

\n \n
;\n}\n\nfunction Field({icon, onIconClick, action, error, className = '', ...props}) {\n return
\n
\n {icon ? (onIconClick ?\n :\n {icon}) : null}\n \n {action}\n
\n {error ?
{error}
: null}\n
;\n}\n\nfunction PasswordField(props) {\n const [visible, setVisible] = React.useState(false);\n return setVisible(!visible)}\n aria-label={visible ? '隐藏密码' : '显示密码'}>{visible ? 'visibility' : 'visibility_off'}} />;\n}\n\nfunction Agreement({checked, onChange, prefix = '同意捷信密使'}) {\n return ;\n}\n\nfunction Toast({message, onDone}) {\n React.useEffect(() => {\n if (!message) return undefined;\n const timer = window.setTimeout(onDone, 2800);\n return () => window.clearTimeout(timer);\n }, [message, onDone]);\n return message ?
check_circle{message}
: null;\n}\n\n// Shared alert for errors coming back from a real API/WS call (as opposed to local form validation,\n// which each form already reports inline per-field).\nfunction FormAlert({text}) {\n return text ?
error_outline{text}
: null;\n}\n\nfunction TenantEntry({onTenantResolved}) {\n const [code, setCode] = React.useState(sessionStorage.getItem('im-tenant-code') || '');\n const [error, setError] = React.useState('');\n const [busy, setBusy] = React.useState(false);\n\n const submit = async e => {\n e.preventDefault();\n const value = code.trim();\n if (!value || value.length > 64) {\n setError(value ? '企业码不能超过 64 个字符' : '请输入企业码');\n return;\n }\n setBusy(true);\n setError('');\n try {\n const data = await resolveTenant(value);\n // Kept for the mock UI's own display of the tenant name while on this screen; the durable\n // context (read by every subsequent /v1/im/* call and by the {hi} tenant field) is persisted\n // by onTenantResolved.\n sessionStorage.setItem('im-tenant-code', data.tenant.code || value);\n sessionStorage.setItem('im-tenant-name', data.tenant.name || data.tenant.code || value);\n onTenantResolved(data, value);\n navigate('login');\n } catch (err) {\n // Per the product spec, tenant-resolve failures are shown as one generic message\n // regardless of reason (not found vs. disabled vs. rate limited).\n setError('企业码不存在或已停用,请核实后重试');\n } finally {\n setBusy(false);\n }\n };\n\n return
\n \n
\n \n { setCode(e.target.value); setError(''); }}\n placeholder=\"请输入企业码\" autoComplete=\"organization\" maxLength=\"64\" error={error} autoFocus />\n \n \n \n
;\n}\n\nfunction Login({tenantName, login, loginDisabled, persist, errorText, errorLevel, onLoginRequest, onPersistenceChange, onError}) {\n const [account, setAccount] = React.useState(login || '');\n const [password, setPassword] = React.useState('');\n const [agreed, setAgreed] = React.useState(false);\n\n // The prototype (docs/images/登录页面.png) has no \"stay logged in\" checkbox -- a consumer IM app\n // just keeps you signed in. Opt in silently instead of exposing the old debug-era toggle.\n React.useEffect(() => {\n if (!persist) {\n onPersistenceChange(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Real login failures (wrong password, disabled account, ...) clear the password and keep the account,\n // matching the product spec.\n React.useEffect(() => {\n if (errorLevel === 'err' && errorText) {\n setPassword('');\n }\n }, [errorText, errorLevel]);\n\n const submit = e => {\n e.preventDefault();\n if (!account.trim() || !password || !agreed || loginDisabled) return;\n onLoginRequest(account.trim(), password);\n };\n\n return
\n \n
\n \n \n setAccount(e.target.value)}\n placeholder=\"请输入用户名/手机号\" autoComplete=\"username\" />\n setPassword(e.target.value)}\n placeholder=\"请输入密码\" autoComplete=\"current-password\" />\n
\n \n

没有账户?

\n \n \n
;\n}\n\nfunction EnterpriseRegister() {\n const [creditCode, setCreditCode] = React.useState('');\n const [email, setEmail] = React.useState('');\n const [file, setFile] = React.useState(null);\n const [errors, setErrors] = React.useState({});\n const [submitting, setSubmitting] = React.useState(false);\n const [submitted, setSubmitted] = React.useState(false);\n const [reviewDays, setReviewDays] = React.useState(1);\n const [toast, setToast] = React.useState('');\n\n React.useEffect(() => () => file && URL.revokeObjectURL(file.url), [file]);\n\n const chooseFile = e => {\n const selected = e.target.files[0];\n if (!selected) return;\n if (selected.size > 10 * 1024 * 1024 || !['image/jpeg', 'image/png', 'image/webp', 'application/pdf'].includes(selected.type)) {\n setErrors({...errors, file: '支持 JPG、PNG、WebP、PDF,文件不超过 10MB'});\n return;\n }\n if (file) URL.revokeObjectURL(file.url);\n setFile({name: selected.name, type: selected.type, url: URL.createObjectURL(selected), blob: selected});\n setErrors({...errors, file: ''});\n };\n\n const submit = async e => {\n e.preventDefault();\n const next = {};\n if (!CREDIT_CODE_RE.test(creditCode)) next.creditCode = '请输入 18 位统一社会信用代码';\n if (email && !EMAIL_RE.test(email)) next.email = '请输入有效的企业邮箱';\n setErrors(next);\n if (Object.keys(next).length) return;\n\n setSubmitting(true);\n try {\n let licenseFileId = '';\n if (file) {\n const uploaded = await uploadPublicFile(file.blob, 'enterprise_license');\n licenseFileId = uploaded.file_id;\n }\n const data = await submitTenantApplication({\n credit_code: creditCode,\n enterprise_email: email,\n license_file_id: licenseFileId,\n tenant_code_hint: sessionStorage.getItem('im-tenant-code') || ''\n });\n // Prototype (docs/images/企业注册-填写注册信息.png) states this in business days, not hours.\n setReviewDays(Math.max(1, Math.round((data.estimated_review_hours || 24) / 24)));\n setSubmitted(true);\n setToast('提交成功,我们会尽快联系你');\n } catch (err) {\n setErrors({...next, form: err.message || '提交失败,请稍后重试'});\n } finally {\n setSubmitting(false);\n }\n };\n\n return
\n
\n
\n
    \n
  1. 1填写企业信息
  2. \n
  3. 2确定信息
  4. \n
  5. 3提交审核
  6. \n
\n
\n \n \n setCreditCode(e.target.value.trim().toUpperCase().replace(/[^0-9A-Z]/g, ''))}\n placeholder=\"请输入18位统一社会信用代码\" maxLength=\"18\" icon=\"qr_code_scanner\" error={errors.creditCode}\n onIconClick={() => setToast('扫码识别暂不可用,请手动输入')} />\n

请填写营业执照上的统一社会信用代码

\n\n \n \n {file ? : null}\n {errors.file ?
{errors.file}
: null}\n\n \n setEmail(e.target.value)}\n placeholder=\"请输入企业邮箱\" type=\"email\" error={errors.email} />\n

用于接收审核结果及企业通知

\n \n

schedule资料提交后,预计 {reviewDays} 个工作日完成审核

\n \n
\n setToast('')} />\n
;\n}\n\n// Per docs/images/企业注册-问题反馈.png: titled \"意见反馈\", 300-char textarea with the instructions\n// as its placeholder (not a separate label), \"图片(N/9)\" counter-style label, and no contact field.\nfunction EnterpriseFeedback() {\n const [description, setDescription] = React.useState('');\n const [files, setFiles] = React.useState([]);\n const [error, setError] = React.useState('');\n const [submitting, setSubmitting] = React.useState(false);\n const [toast, setToast] = React.useState('');\n\n const addFiles = e => {\n const selected = Array.from(e.target.files || []).slice(0, 9 - files.length);\n const valid = selected.filter(file => file.size <= 10 * 1024 * 1024 && ['image/jpeg', 'image/png', 'image/webp'].includes(file.type));\n setFiles(files.concat(valid.map(file => ({name: file.name, url: URL.createObjectURL(file), blob: file}))));\n };\n const submit = async e => {\n e.preventDefault();\n const length = description.trim().length;\n if (length < 10 || length > 300) {\n setError('问题描述需为 10 至 300 个字符');\n return;\n }\n setSubmitting(true);\n try {\n const screenshotFileIds = [];\n for (const item of files) {\n const uploaded = await uploadPublicFile(item.blob, 'tenant_entry_feedback');\n screenshotFileIds.push(uploaded.file_id);\n }\n await submitTenantEntryFeedback({\n description: description.trim(),\n tenant_code_hint: sessionStorage.getItem('im-tenant-code') || '',\n screenshot_file_ids: screenshotFileIds\n });\n setToast('已收到反馈,我们会尽快处理');\n files.forEach(item => URL.revokeObjectURL(item.url));\n setDescription(''); setFiles([]); setError('');\n } catch (err) {\n setError(err.message || '提交失败,请稍后重试');\n } finally {\n setSubmitting(false);\n }\n };\n\n return
\n
\n
\n
\n \n