增加前端格式检查配置

This commit is contained in:
yml2213
2026-06-08 06:40:33 +08:00
parent 79ea3a476c
commit 500f511185
150 changed files with 7254 additions and 4013 deletions
@@ -1,256 +1,256 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useSessionStore } from "@/stores/session";
import { showDialog, showToast } from "vant";
import MobileBottomNav from "@/components/MobileBottomNav.vue";
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useSessionStore } from '@/stores/session'
import { showDialog, showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { fetchWalletBalance, fetchWalletLedger, type WalletLedger } from "@/features/wallet/api/wallet";
import { fetchPostRentalNotice, type PostRentalNotice } from "@/features/orders/api/orders";
import { formatDateMinute } from "@/utils/time";
import { uploadFile } from "@/shared/api/files";
import {
fetchWalletBalance,
fetchWalletLedger,
type WalletLedger,
} from '@/features/wallet/api/wallet'
import { fetchPostRentalNotice, type PostRentalNotice } from '@/features/orders/api/orders'
import { formatDateMinute } from '@/utils/time'
import { uploadFile } from '@/shared/api/files'
const session = useSessionStore();
const router = useRouter();
const route = useRoute();
const session = useSessionStore()
const router = useRouter()
const route = useRoute()
/** 数据项 */
const balance = ref(0);
const showSettings = ref(false);
const showProfileEditor = ref(false);
const savingProfile = ref(false);
const balance = ref(0)
const showSettings = ref(false)
const showProfileEditor = ref(false)
const savingProfile = ref(false)
const profileForm = reactive({
nickname: "",
avatar_url: "",
});
nickname: '',
avatar_url: '',
})
const avatarFileInput = ref<HTMLInputElement | null>(null);
const uploadingAvatar = ref(false);
const avatarFileInput = ref<HTMLInputElement | null>(null)
const uploadingAvatar = ref(false)
function triggerAvatarUpload() {
avatarFileInput.value?.click();
avatarFileInput.value?.click()
}
async function handleAvatarFileChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = "";
if (!file) return;
const input = event.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) return
uploadingAvatar.value = true;
uploadingAvatar.value = true
const toast = showToast({
type: "loading",
message: "上传中...",
type: 'loading',
message: '上传中...',
forbidClick: true,
duration: 0,
});
})
try {
const uploaded = await uploadFile(file, "avatar");
profileForm.avatar_url = uploaded.url;
showToast({ message: "上传成功", icon: "passed" });
const uploaded = await uploadFile(file, 'avatar')
profileForm.avatar_url = uploaded.url
showToast({ message: '上传成功', icon: 'passed' })
} catch (error) {
showToast({ message: readError(error, "上传失败"), icon: "cross" });
showToast({ message: readError(error, '上传失败'), icon: 'cross' })
} finally {
uploadingAvatar.value = false;
uploadingAvatar.value = false
}
}
// 收支明细列表
const showLedgers = ref(false);
const loadingLedgers = ref(false);
const ledgers = ref<WalletLedger[]>([]);
const showLedgers = ref(false)
const loadingLedgers = ref(false)
const ledgers = ref<WalletLedger[]>([])
// 租后须知
const showPostRentalNotice = ref(false);
const loadingPostRentalNotice = ref(false);
const postRentalNotice = ref<PostRentalNotice | null>(null);
const showPostRentalNotice = ref(false)
const loadingPostRentalNotice = ref(false)
const postRentalNotice = ref<PostRentalNotice | null>(null)
const settingsGroups = [
{
title: "账号与安全",
title: '账号与安全',
items: [
{ label: "资料更改", icon: "edit", action: "profile" },
{ label: "实名认证", icon: "idcard", action: "realname" },
{ label: "注销账号", icon: "delete-o", action: "cancel-account" },
{ label: '资料更改', icon: 'edit', action: 'profile' },
{ label: '实名认证', icon: 'idcard', action: 'realname' },
{ label: '注销账号', icon: 'delete-o', action: 'cancel-account' },
],
},
{
title: "法律与隐私",
title: '法律与隐私',
items: [
{ label: "隐私政策", icon: "shield-o", action: "privacy" },
{ label: "用户协议", icon: "description", action: "terms" },
{ label: '隐私政策', icon: 'shield-o', action: 'privacy' },
{ label: '用户协议', icon: 'description', action: 'terms' },
],
},
];
]
onMounted(() => {
if (!isLoggedIn.value) {
router.replace({ path: "/m/login", query: { redirect: route.fullPath } });
router.replace({ path: '/m/login', query: { redirect: route.fullPath } })
} else {
loadBalance();
loadBalance()
}
});
})
async function loadBalance() {
try {
const wallet = await fetchWalletBalance();
balance.value = wallet.available_balance;
const wallet = await fetchWalletBalance()
balance.value = wallet.available_balance
} catch {
// 失败静默处理,显示为默认0
}
}
async function openLedgers() {
showLedgers.value = true;
loadingLedgers.value = true;
showLedgers.value = true
loadingLedgers.value = true
try {
const res = await fetchWalletLedger(1, 20);
ledgers.value = res.items;
const res = await fetchWalletLedger(1, 20)
ledgers.value = res.items
} catch {
showToast({ message: "无法获取账单明细", icon: "cross" });
showToast({ message: '无法获取账单明细', icon: 'cross' })
} finally {
loadingLedgers.value = false;
loadingLedgers.value = false
}
}
async function openPostRentalNotice() {
showPostRentalNotice.value = true;
showPostRentalNotice.value = true
if (!postRentalNotice.value) {
loadingPostRentalNotice.value = true;
loadingPostRentalNotice.value = true
try {
postRentalNotice.value = await fetchPostRentalNotice();
postRentalNotice.value = await fetchPostRentalNotice()
} catch {
showToast({ message: "无法获取租后须知", icon: "cross" });
showToast({ message: '无法获取租后须知', icon: 'cross' })
} finally {
loadingPostRentalNotice.value = false;
loadingPostRentalNotice.value = false
}
}
}
function handleWithdraw() {
showDialog({
title: "提现提示",
message: "为了您的资金安全,提现请前往电脑端网页版进行操作。",
});
title: '提现提示',
message: '为了您的资金安全,提现请前往电脑端网页版进行操作。',
})
}
function goOrders(tabKey: string) {
router.push({ path: "/m/orders", query: { tab: tabKey } });
router.push({ path: '/m/orders', query: { tab: tabKey } })
}
function onSettingClick(action: string) {
showSettings.value = false;
showSettings.value = false
switch (action) {
case "profile":
openProfileEditor();
break;
case "realname":
router.push("/m/realname");
break;
case "cancel-account":
case 'profile':
openProfileEditor()
break
case 'realname':
router.push('/m/realname')
break
case 'cancel-account':
showDialog({
title: "注销账号",
message: "注销后账号数据将无法恢复,确认注销吗?",
title: '注销账号',
message: '注销后账号数据将无法恢复,确认注销吗?',
showCancelButton: true,
confirmButtonText: "确认注销",
confirmButtonColor: "#ee0a24",
cancelButtonText: "再想想",
confirmButtonText: '确认注销',
confirmButtonColor: '#ee0a24',
cancelButtonText: '再想想',
})
.then(() => {
showToast({ message: "注销功能开发中", icon: "info-o" });
showToast({ message: '注销功能开发中', icon: 'info-o' })
})
.catch(() => {});
break;
case "privacy":
showToast({ message: "隐私政策页面开发中", icon: "info-o" });
break;
case "terms":
showToast({ message: "用户协议页面开发中", icon: "info-o" });
break;
.catch(() => {})
break
case 'privacy':
showToast({ message: '隐私政策页面开发中', icon: 'info-o' })
break
case 'terms':
showToast({ message: '用户协议页面开发中', icon: 'info-o' })
break
}
}
function openProfileEditor() {
profileForm.nickname = session.nickname || defaultName.value;
profileForm.avatar_url = session.avatarUrl || "";
showProfileEditor.value = true;
profileForm.nickname = session.nickname || defaultName.value
profileForm.avatar_url = session.avatarUrl || ''
showProfileEditor.value = true
}
async function saveProfile() {
const nickname = profileForm.nickname.trim();
const avatarURL = profileForm.avatar_url.trim();
const nickname = profileForm.nickname.trim()
const avatarURL = profileForm.avatar_url.trim()
if (!nickname) {
showToast({ message: "请输入昵称", icon: "warning-o" });
return;
showToast({ message: '请输入昵称', icon: 'warning-o' })
return
}
if (nickname.length > 24) {
showToast({ message: "昵称不能超过 24 个字符", icon: "warning-o" });
return;
showToast({ message: '昵称不能超过 24 个字符', icon: 'warning-o' })
return
}
savingProfile.value = true;
savingProfile.value = true
try {
await session.updateProfile({ nickname, avatar_url: avatarURL });
showProfileEditor.value = false;
showToast({ message: "资料已更新", icon: "passed" });
await loadBalance();
await session.updateProfile({ nickname, avatar_url: avatarURL })
showProfileEditor.value = false
showToast({ message: '资料已更新', icon: 'passed' })
await loadBalance()
} catch (error) {
showToast({ message: readError(error, "资料更新失败"), icon: "cross" });
showToast({ message: readError(error, '资料更新失败'), icon: 'cross' })
} finally {
savingProfile.value = false;
savingProfile.value = false
}
}
function readError(error: unknown, fallback: string) {
if (typeof error === "object" && error && "response" in error) {
const response = (error as { response?: { data?: { message?: string } } }).response;
return response?.data?.message || fallback;
if (typeof error === 'object' && error && 'response' in error) {
const response = (error as { response?: { data?: { message?: string } } }).response
return response?.data?.message || fallback
}
return fallback;
return fallback
}
function confirmLogout() {
showSettings.value = false;
showSettings.value = false
showDialog({
title: "退出登录",
message: "确定要退出当前账号吗?",
title: '退出登录',
message: '确定要退出当前账号吗?',
showCancelButton: true,
confirmButtonText: "退出",
confirmButtonColor: "#ee0a24",
cancelButtonText: "取消",
confirmButtonText: '退出',
confirmButtonColor: '#ee0a24',
cancelButtonText: '取消',
})
.then(() => {
session.logout();
router.replace("/m");
session.logout()
router.replace('/m')
})
.catch(() => {});
.catch(() => {})
}
const isLoggedIn = computed(() => Boolean(session.token));
const isRealnameVerified = computed(() => session.realnameStatus === "verified");
const isRealnamePending = computed(() => session.realnameStatus === "pending");
const isLoggedIn = computed(() => Boolean(session.token))
const isRealnameVerified = computed(() => session.realnameStatus === 'verified')
const isRealnamePending = computed(() => session.realnameStatus === 'pending')
const defaultName = computed(() =>
session.phone
? `用户${session.phone.slice(-6)}`
: `用户${session.userId || 883099}`
);
const displayName = computed(() => session.nickname || defaultName.value);
const displayId = computed(() => session.userId || 138865);
session.phone ? `用户${session.phone.slice(-6)}` : `用户${session.userId || 883099}`
)
const displayName = computed(() => session.nickname || defaultName.value)
const displayId = computed(() => session.userId || 138865)
const maskedPhone = computed(() =>
session.phone
? `${session.phone.slice(0, 3)}****${session.phone.slice(-4)}`
: "登录后查看手机号"
);
const avatarText = computed(() => displayName.value.slice(0, 1));
session.phone ? `${session.phone.slice(0, 3)}****${session.phone.slice(-4)}` : '登录后查看手机号'
)
const avatarText = computed(() => displayName.value.slice(0, 1))
function resolveAvatarURL(url: string | undefined | null) {
if (!url) return "";
if (url.includes("/api/files/object?key=avatar/")) {
return url.replace("/api/files/object", "/api/public/files/object");
if (!url) return ''
if (url.includes('/api/files/object?key=avatar/')) {
return url.replace('/api/files/object', '/api/public/files/object')
}
return url;
return url
}
</script>
@@ -276,13 +276,32 @@ function resolveAvatarURL(url: string | undefined | null) {
<div class="hero-user">
<div class="name-row">
<h1 class="user-nickname">{{ displayName }}</h1>
<van-tag v-if="isRealnameVerified" type="success" size="medium" round class="verified-tag">
<van-tag
v-if="isRealnameVerified"
type="success"
size="medium"
round
class="verified-tag"
>
<van-icon name="shield" /> 已实名
</van-tag>
<van-tag v-else-if="isRealnamePending" type="warning" size="medium" round class="verified-tag">
<van-tag
v-else-if="isRealnamePending"
type="warning"
size="medium"
round
class="verified-tag"
>
审核中
</van-tag>
<van-tag v-else type="danger" size="medium" round class="unverified-tag" @click="router.push('/m/realname')">
<van-tag
v-else
type="danger"
size="medium"
round
class="unverified-tag"
@click="router.push('/m/realname')"
>
未实名
</van-tag>
</div>
@@ -416,7 +435,9 @@ function resolveAvatarURL(url: string | undefined | null) {
<button class="popup-close" @click="showPostRentalNotice = false"></button>
</header>
<div class="popup-body">
<van-loading v-if="loadingPostRentalNotice" class="center-loading" vertical>加载中...</van-loading>
<van-loading v-if="loadingPostRentalNotice" class="center-loading" vertical
>加载中...</van-loading
>
<div v-else-if="postRentalNotice" class="notice-content">
{{ postRentalNotice.content }}
</div>
@@ -438,11 +459,7 @@ function resolveAvatarURL(url: string | undefined | null) {
</button>
</header>
<div
v-for="group in settingsGroups"
:key="group.title"
class="settings-group"
>
<div v-for="group in settingsGroups" :key="group.title" class="settings-group">
<h3 class="settings-group-title">{{ group.title }}</h3>
<van-cell-group :border="false">
<van-cell
@@ -457,9 +474,7 @@ function resolveAvatarURL(url: string | undefined | null) {
</div>
<div class="settings-logout">
<van-button plain type="danger" block round @click="confirmLogout">
退出登录
</van-button>
<van-button plain type="danger" block round @click="confirmLogout"> 退出登录 </van-button>
</div>
</div>
</van-popup>
@@ -481,7 +496,11 @@ function resolveAvatarURL(url: string | undefined | null) {
<div class="profile-preview">
<div class="profile-preview-avatar clickable" @click="triggerAvatarUpload">
<img v-if="profileForm.avatar_url" :src="resolveAvatarURL(profileForm.avatar_url)" alt="" />
<img
v-if="profileForm.avatar_url"
:src="resolveAvatarURL(profileForm.avatar_url)"
alt=""
/>
<span v-else>{{ (profileForm.nickname || defaultName).slice(0, 1) }}</span>
<div class="avatar-upload-overlay">
<van-icon name="photograph" :size="16" />
@@ -516,11 +535,7 @@ function resolveAvatarURL(url: string | undefined | null) {
<label class="editor-field">
<span>昵称</span>
<input
v-model="profileForm.nickname"
maxlength="24"
placeholder="请输入昵称"
/>
<input v-model="profileForm.nickname" maxlength="24" placeholder="请输入昵称" />
</label>
<label class="editor-field">
@@ -640,7 +655,8 @@ function resolveAvatarURL(url: string | undefined | null) {
color: #111827;
}
.verified-tag, .unverified-tag {
.verified-tag,
.unverified-tag {
font-size: 10px !important;
height: 18px;
padding: 0 6px;
@@ -660,7 +676,9 @@ function resolveAvatarURL(url: string | undefined | null) {
padding: 16px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.02), 0 1px 4px rgba(0, 0, 0, 0.02);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.02),
0 1px 4px rgba(0, 0, 0, 0.02);
border: 1px solid rgba(243, 244, 246, 0.9);
}
@@ -699,7 +717,9 @@ function resolveAvatarURL(url: string | undefined | null) {
padding: 0 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
transition:
background 0.15s,
color 0.15s;
}
.withdraw-btn:active {
@@ -732,7 +752,9 @@ function resolveAvatarURL(url: string | undefined | null) {
background: #ffffff;
border-radius: 16px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.02), 0 1px 4px rgba(0, 0, 0, 0.02);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.02),
0 1px 4px rgba(0, 0, 0, 0.02);
border: 1px solid rgba(243, 244, 246, 0.9);
}
@@ -801,13 +823,34 @@ function resolveAvatarURL(url: string | undefined | null) {
}
/* Vibrant pastel colors for menu icon wraps */
.icon-wrap.warning { color: #f59e0b; background: rgba(245, 158, 11, 0.08); }
.icon-wrap.info { color: #2563eb; background: rgba(37, 99, 235, 0.08); }
.icon-wrap.primary { color: #1477ff; background: rgba(20, 119, 255, 0.08); }
.icon-wrap.success { color: #10b981; background: rgba(16, 185, 129, 0.08); }
.icon-wrap.orange { color: #ff5f00; background: rgba(255, 95, 0, 0.08); }
.icon-wrap.purple { color: #8b5cf6; background: rgba(139, 92, 246, 0.08); }
.icon-wrap.teal { color: #0d9488; background: rgba(13, 148, 136, 0.08); }
.icon-wrap.warning {
color: #f59e0b;
background: rgba(245, 158, 11, 0.08);
}
.icon-wrap.info {
color: #2563eb;
background: rgba(37, 99, 235, 0.08);
}
.icon-wrap.primary {
color: #1477ff;
background: rgba(20, 119, 255, 0.08);
}
.icon-wrap.success {
color: #10b981;
background: rgba(16, 185, 129, 0.08);
}
.icon-wrap.orange {
color: #ff5f00;
background: rgba(255, 95, 0, 0.08);
}
.icon-wrap.purple {
color: #8b5cf6;
background: rgba(139, 92, 246, 0.08);
}
.icon-wrap.teal {
color: #0d9488;
background: rgba(13, 148, 136, 0.08);
}
.grid-item span {
font-size: 11px;