release: prepare August 1 Android RC

This commit is contained in:
key
2026-07-29 00:35:15 +08:00
parent 9ae5f750c8
commit 9ea5d79a62
41 changed files with 2322 additions and 71 deletions
+5
View File
@@ -1,7 +1,12 @@
/// <reference types="@dcloudio/types" />
interface ImportMetaEnv {
readonly DEV: boolean;
readonly VITE_API_BASE?: string;
readonly VITE_APP_TERMS_URL?: string;
readonly VITE_APP_PRIVACY_URL?: string;
readonly VITE_DEV_SMS_HINT_ENABLED?: string;
readonly VITE_DEV_SMS_HINT_VALUE?: string;
}
interface ImportMeta {
+10 -2
View File
@@ -3,8 +3,8 @@
"appid" : "__UNI__B36D8BE",
"description" : "帮道员工端",
"vueVersion" : "3",
"versionName" : "0.1.5",
"versionCode" : "105",
"versionName" : "0.1.6",
"versionCode" : "106",
"uniStatistics" : {
"enable" : false
},
@@ -59,6 +59,7 @@
},
"splashscreen" : {
"iosStyle" : "default",
"useOriginalMsgbox" : true,
"ios" : {
"iphone" : {
"retina40" : "src/static/app/splash/ios-iphone-retina40.png",
@@ -87,7 +88,14 @@
},
"android" : {
"packagename" : "com.yincheng.wygj",
"targetSdkVersion" : 35,
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
"permissionExternalStorage" : {
"request" : "none"
},
"permissionPhoneState" : {
"request" : "none"
},
"permissions" : [
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>"
+89 -14
View File
@@ -74,9 +74,9 @@
</uni-forms-item>
</uni-forms>
<view class="dev-hint-pill" @click="fillDevCredentials">
<view v-if="showDevSmsHint" class="dev-hint-pill" @click="fillDevCredentials">
<uni-icons type="info-filled" size="14" color="#d97706" />
<text class="dev-hint-text">测试验证码: 123456</text>
<text class="dev-hint-text">测试验证码: {{ devSmsHintValue }}</text>
<text class="dev-fill-btn">快捷填入</text>
</view>
@@ -93,12 +93,25 @@
/>
</view>
<view class="agreement-row" @click="agreed = !agreed">
<view class="checkbox-box" :class="{ checked: agreed }">
<uni-icons v-if="agreed" type="checkmarkempty" size="12" color="#ffffff" />
<view class="agreement-row">
<view
class="agreement-consent"
role="checkbox"
:aria-checked="agreed"
@click="agreed = !agreed"
>
<view class="checkbox-box" :class="{ checked: agreed }">
<uni-icons v-if="agreed" type="checkmarkempty" size="12" color="#ffffff" />
</view>
<text class="agreement-text">已阅读并同意</text>
</view>
<text class="agreement-text">已阅读并同意《服务协议》与《隐私政策》</text>
<text class="agreement-link" @click.stop="openLegalDocument('terms')">《服务协议》</text>
<text class="agreement-text">与</text>
<text class="agreement-link" @click.stop="openLegalDocument('privacy')">《隐私政策》</text>
</view>
<text v-if="!legalLinksReady" class="agreement-error">
协议地址尚未完成法务配置,当前暂停验证码发送与登录。
</text>
</view>
</view>
</template>
@@ -109,6 +122,11 @@ import { onHide, onShow, onUnload } from '@dcloudio/uni-app';
import type { LoginResult } from '@/types/api';
import { apiRequest } from '@/services/api';
import { consumeLoginRedirect, getAuth, isLoggedIn, phoneKey, saveAuth } from '@/services/auth';
import {
legalDocumentUrl,
legalLinksReady,
type LegalDocumentKind
} from '@/services/legal';
import { resetPageScroll } from '@/services/navigation';
import { goToMobileTarget, nextUserRouteAfterLogin } from '@/services/position';
@@ -135,8 +153,14 @@ const formRules = {
const sending = ref(false);
const loggingIn = ref(false);
const countdown = ref(0);
const agreed = ref(true);
const agreed = ref(false);
const message = ref('');
const devSmsHintValue = import.meta.env.DEV
? String(import.meta.env.VITE_DEV_SMS_HINT_VALUE || '').replace(/\D/g, '').slice(0, 6)
: '';
const showDevSmsHint = import.meta.env.DEV
&& import.meta.env.VITE_DEV_SMS_HINT_ENABLED === 'true'
&& /^\d{4,6}$/.test(devSmsHintValue);
let timer: ReturnType<typeof setInterval> | null = null;
const inputStyles = {
@@ -166,8 +190,9 @@ const startCountdown = () => {
};
const fillDevCredentials = () => {
formData.smsCode = '123456';
message.value = '已自动填入测试验证码 123456';
if (!showDevSmsHint) return;
formData.smsCode = devSmsHintValue;
message.value = '已自动填入本地测试验证码';
};
const normalizeDigits = (value: string | number, maxLength: number) =>
@@ -183,6 +208,33 @@ const onSmsCodeInput = (value: string | number) => {
formData.smsCode = normalizeDigits(value, 6);
};
const ensureLegalConsent = () => {
if (!legalLinksReady) {
message.value = '服务协议和隐私政策地址尚未配置,暂不能发送验证码或登录';
return false;
}
if (!agreed.value) {
message.value = '请先阅读并勾选同意《服务协议》与《隐私政策》';
return false;
}
return true;
};
const openLegalDocument = (kind: LegalDocumentKind) => {
const url = legalDocumentUrl(kind);
if (!url) {
message.value = '协议地址尚未完成法务配置';
return;
}
if (typeof plus !== 'undefined') {
plus.runtime.openURL(url);
return;
}
if (typeof window !== 'undefined') {
window.open(url, '_blank', 'noopener,noreferrer');
}
};
const goAfterLogin = async () => {
const redirect = consumeLoginRedirect() || '/pages/user/today/index';
goToMobileTarget(await nextUserRouteAfterLogin(redirect));
@@ -190,6 +242,7 @@ const goAfterLogin = async () => {
const sendCode = async () => {
if (sending.value || countdown.value > 0) return;
if (!ensureLegalConsent()) return;
try {
await loginForm.value?.validateField(['phone']);
} catch {
@@ -215,10 +268,7 @@ const sendCode = async () => {
const login = async () => {
if (loggingIn.value) return;
if (!agreed.value) {
message.value = '请先勾选同意《服务协议》与《隐私政策》';
return;
}
if (!ensureLegalConsent()) return;
try {
await loginForm.value?.validate();
} catch {
@@ -616,9 +666,16 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
gap: 2px;
max-width: 720px;
margin: 18px auto 0;
flex-wrap: wrap;
}
.agreement-consent {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
@@ -647,6 +704,24 @@ onUnmounted(() => {
line-height: 1.4;
}
.agreement-link {
color: var(--employee-primary, #e60012);
font-size: 12px;
font-weight: 650;
line-height: 1.4;
cursor: pointer;
}
.agreement-error {
display: block;
max-width: 720px;
margin: 8px auto 0;
color: #b45309;
font-size: 11px;
line-height: 1.5;
text-align: center;
}
@media (max-width: 375px) {
.login-shell {
padding-right: 20px;
+1 -1
View File
@@ -426,7 +426,7 @@ const submit = async () => {
form.requestId = '';
form.content = '';
voiceHint.value = '';
submitMessage.value = '反馈已提交,可在“我的反馈”查看处理结果。';
submitMessage.value = '已收到,当前状态为“待处理”;可在“我的反馈”查看人工正式回复。';
await loadMine(true);
} catch (error) {
submitFailed.value = true;
+5 -1
View File
@@ -517,7 +517,11 @@ type ChatMsg = UserMsg | MasterMsg | SystemMsg | PendingMsg;
const positionProfile = ref(getSelectedPositionProfile());
const positionLabel = ref(getSelectedPosition());
const quickQuestions = computed(() => positionProfile.value.quickQuestions);
const policyQuickQuestion = '问制度:请说明公司请假和考勤的适用规定';
const quickQuestions = computed(() => [
policyQuickQuestion,
...positionProfile.value.quickQuestions.filter((item) => item !== policyQuickQuestion)
].slice(0, 4));
const quickQuestionPlaceholder = computed(() => `例如: ${quickQuestions.value[0] || '岗位SOP怎么处理?'}`);
const messages = ref<ChatMsg[]>([]);
+81
View File
@@ -0,0 +1,81 @@
export type LegalDocumentKind = 'terms' | 'privacy';
const placeholderPattern = /__|change[-_ ]?me|your[-_ ]?|example\.(com|org|net)/i;
const reservedHostnamePattern = /(?:^|\.)(?:example|invalid|test)$/i;
const ipv4Parts = (hostname: string) => {
const parts = hostname.split('.').map(Number);
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
return null;
}
return parts;
};
const isReservedIpv4 = (parts: number[]) => {
return parts[0] === 0
|| parts[0] === 10
|| parts[0] === 127
|| (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127)
|| (parts[0] === 169 && parts[1] === 254)
|| (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|| (parts[0] === 192 && parts[1] === 168)
|| (parts[0] === 192 && parts[1] === 0 && (parts[2] === 0 || parts[2] === 2))
|| (parts[0] === 198 && (parts[1] === 18 || parts[1] === 19))
|| (parts[0] === 198 && parts[1] === 51 && parts[2] === 100)
|| (parts[0] === 203 && parts[1] === 0 && parts[2] === 113)
|| parts[0] >= 224;
};
const normalizePublicHttpsUrl = (rawValue?: string) => {
const value = String(rawValue || '').trim();
if (!value || placeholderPattern.test(value)) return '';
if (/[\u0000-\u0020\u007f]/.test(value)) return '';
const match = /^https:\/\/([^/?#]+)(?:[/?#]|$)/i.exec(value);
if (!match || match[1].includes('@')) return '';
const authority = match[1];
let hostname = '';
if (authority.startsWith('[')) {
const end = authority.indexOf(']');
if (end <= 1 || (authority.slice(end + 1) && !/^:\d+$/.test(authority.slice(end + 1)))) return '';
hostname = authority.slice(1, end).toLowerCase();
} else {
const hostAndPort = authority.toLowerCase();
const colon = hostAndPort.lastIndexOf(':');
if (colon >= 0) {
if (!/^\d+$/.test(hostAndPort.slice(colon + 1)) || hostAndPort.slice(0, colon).includes(':')) return '';
hostname = hostAndPort.slice(0, colon);
} else {
hostname = hostAndPort;
}
}
const numericIpv4 = /^[0-9.]+$/.test(hostname);
const parsedIpv4 = numericIpv4 ? ipv4Parts(hostname) : null;
if (
!hostname
|| reservedHostnamePattern.test(hostname)
|| hostname === 'localhost'
|| hostname.endsWith('.localhost')
|| hostname === '::1'
|| (numericIpv4 && (!parsedIpv4 || isReservedIpv4(parsedIpv4)))
|| (hostname.includes(':') && (!/^[0-9a-f:]+$/i.test(hostname) || /^f[cd]/.test(hostname) || hostname.startsWith('fe80:')))
|| (!numericIpv4 && !hostname.includes(':') && (
!hostname.includes('.')
|| !/^[a-z0-9.-]+$/i.test(hostname)
|| hostname.startsWith('.')
|| hostname.endsWith('.')
))
) return '';
return value;
};
const termsUrl = normalizePublicHttpsUrl(import.meta.env.VITE_APP_TERMS_URL);
const privacyUrl = normalizePublicHttpsUrl(import.meta.env.VITE_APP_PRIVACY_URL);
export const legalLinksReady = Boolean(termsUrl && privacyUrl && termsUrl !== privacyUrl);
export const legalDocumentUrl = (kind: LegalDocumentKind) => {
if (!legalLinksReady) return '';
return kind === 'terms' ? termsUrl : privacyUrl;
};