Initial project commit
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
|
||||
import { getFavorites } from "@/api/favorite";
|
||||
import { extractList } from "@/utils/request";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
import SpotCard from "@/components/spot-card/spot-card.vue";
|
||||
|
||||
onShow(() => { checkLogin(); });
|
||||
|
||||
const favorites = ref([]);
|
||||
const page = ref(1);
|
||||
const pageSize = 10;
|
||||
const hasMore = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchFavorites = async (reset = false) => {
|
||||
if (loading.value) return;
|
||||
if (!reset && !hasMore.value) return;
|
||||
|
||||
loading.value = true;
|
||||
if (reset) {
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getFavorites({ page: page.value, page_size: pageSize });
|
||||
const list = extractList(res);
|
||||
|
||||
if (reset) {
|
||||
favorites.value = list;
|
||||
} else {
|
||||
favorites.value.push(...list);
|
||||
}
|
||||
|
||||
if (list.length < pageSize) {
|
||||
hasMore.value = false;
|
||||
} else {
|
||||
page.value++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goDetail = (item) => {
|
||||
const id = item.spot_id || item.spot?.id || item.id;
|
||||
if (id) {
|
||||
uni.navigateTo({ url: `/pages/spot/detail?id=${id}` });
|
||||
}
|
||||
};
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchFavorites(true);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
fetchFavorites();
|
||||
});
|
||||
|
||||
fetchFavorites(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="favorites-page">
|
||||
<view class="list">
|
||||
<SpotCard
|
||||
v-for="item in favorites"
|
||||
:key="item.id"
|
||||
:spot="item.spot || item"
|
||||
@click="goDetail(item)"
|
||||
/>
|
||||
|
||||
<view v-if="loading" class="status-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!hasMore && favorites.length > 0" class="status-tip">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
<view v-else-if="!loading && favorites.length === 0" class="empty-state">
|
||||
<uni-icons type="heart-filled" size="48" color="#6366f1" class="empty-icon" />
|
||||
<text class="empty-title">暂无收藏</text>
|
||||
<text class="empty-desc">去发现页浏览并收藏喜欢的取景地吧</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.favorites-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.status-tip {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #94a3b8;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 160rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,403 @@
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { resolveImageUrl } from "@/utils/image";
|
||||
import { getMyStats } from "@/api/user";
|
||||
import { getUnreadCount } from "@/api/notification";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isLoggedIn = computed(() => userStore.isLoggedIn);
|
||||
const user = computed(() => userStore.userInfo);
|
||||
const stats = ref({ spot_count: 0, approved_count: 0, favorite_count: 0, rating_received: 0 });
|
||||
const unreadCount = ref(0);
|
||||
|
||||
const goLogin = () => {
|
||||
uni.navigateTo({ url: "/pages/login/index" });
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
userStore.logout();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ label: "编辑资料", icon: "gear-filled", action: "profile" },
|
||||
{ label: "我的收藏", icon: "heart-filled", action: "favorites" },
|
||||
{ label: "我的投稿", icon: "location-filled", action: "my-spots" },
|
||||
{ label: "约拍/活动", icon: "calendar-filled", action: "activity-hub" },
|
||||
{ label: "会员中心", icon: "vip-filled", action: "membership" },
|
||||
{ label: "积分概览", icon: "star-filled", action: "points" },
|
||||
{ label: "消息通知", icon: "chat-filled", action: "notifications" },
|
||||
{ label: "设置", icon: "gear", action: "settings" },
|
||||
];
|
||||
|
||||
const handleMenu = (action) => {
|
||||
switch (action) {
|
||||
case "profile":
|
||||
uni.navigateTo({ url: "/pages/mine/profile" });
|
||||
break;
|
||||
case "favorites":
|
||||
uni.navigateTo({ url: "/pages/mine/favorites" });
|
||||
break;
|
||||
case "my-spots":
|
||||
uni.navigateTo({ url: "/pages/mine/my-spots" });
|
||||
break;
|
||||
case "points":
|
||||
uni.navigateTo({ url: "/pages/mine/points" });
|
||||
break;
|
||||
case "activity-hub":
|
||||
uni.switchTab({ url: "/pages/activity/index" });
|
||||
break;
|
||||
case "membership":
|
||||
uni.navigateTo({ url: "/pages/mine/membership" });
|
||||
break;
|
||||
case "notifications":
|
||||
uni.switchTab({ url: "/pages/mine/notifications" });
|
||||
break;
|
||||
case "settings":
|
||||
uni.navigateTo({ url: "/pages/mine/settings" });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
|
||||
onShow(async () => {
|
||||
if (isLoggedIn.value) {
|
||||
userStore.fetchUserInfo();
|
||||
try {
|
||||
const s = await getMyStats();
|
||||
stats.value = s;
|
||||
} catch (e) { /* ignore */ }
|
||||
try {
|
||||
const r = await getUnreadCount();
|
||||
unreadCount.value = r.count || 0;
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mine-page">
|
||||
<view v-if="isLoggedIn" class="user-card">
|
||||
<view class="avatar-area">
|
||||
<image
|
||||
v-if="user?.avatar_url"
|
||||
class="avatar"
|
||||
:src="resolveImageUrl(user.avatar_url)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar avatar-default">
|
||||
<uni-icons type="person" size="28" color="#ffffff" class="avatar-icon" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="nickname">{{ user?.nickname || "加载中..." }}</text>
|
||||
<text v-if="user?.bio" class="user-bio">{{ user.bio }}</text>
|
||||
<view class="user-meta">
|
||||
<view v-if="user?.city" class="meta-item"><uni-icons type="location" size="14" color="rgba(255,255,255,0.8)" /> {{ user.city }}</view>
|
||||
<view v-if="user?.identity" class="identity-badge">
|
||||
<text class="identity-text">{{ user.identity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="login-card">
|
||||
<view class="login-avatar">
|
||||
<uni-icons type="person" size="28" color="#6366f1" class="login-avatar-icon" />
|
||||
</view>
|
||||
<text class="login-hint">请先登录</text>
|
||||
<button class="login-btn" @tap="goLogin">立即登录</button>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoggedIn" class="stats-card">
|
||||
<view class="stat-item" @tap="handleMenu('my-spots')">
|
||||
<text class="stat-num">{{ stats.spot_count }}</text>
|
||||
<text class="stat-label">投稿</text>
|
||||
</view>
|
||||
<view class="stat-item" @tap="handleMenu('favorites')">
|
||||
<text class="stat-num">{{ stats.favorite_count }}</text>
|
||||
<text class="stat-label">收藏</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{ stats.rating_received }}</text>
|
||||
<text class="stat-label">获赞</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{ stats.approved_count }}</text>
|
||||
<text class="stat-label">通过</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-card">
|
||||
<view
|
||||
v-for="(item, idx) in menuItems"
|
||||
:key="idx"
|
||||
class="menu-item"
|
||||
@tap="handleMenu(item.action)"
|
||||
>
|
||||
<view class="menu-left">
|
||||
<uni-icons :type="item.icon" size="22" color="#6366f1" class="menu-icon" />
|
||||
<text class="menu-label">{{ item.label }}</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<view v-if="item.action === 'notifications' && unreadCount > 0" class="badge">
|
||||
<text class="badge-text">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#cbd5e1" class="menu-arrow" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoggedIn" class="logout-area">
|
||||
<button class="logout-btn" @tap="handleLogout">退出登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mine-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding: 0 32rpx;
|
||||
padding-top: 32rpx;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background: linear-gradient(135deg, #6366f1, #818cf8);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.avatar-area {
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.avatar-default {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-bio {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 400rpx;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.identity-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.identity-text {
|
||||
font-size: 22rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.login-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
background: #e0e7ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.login-avatar-icon {
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
font-size: 30rpx;
|
||||
color: #64748b;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 320rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #6366f1;
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 40rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.login-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx 0;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.stat-num {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.menu-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.badge {
|
||||
background: #ef4444;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
.badge-text {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-size: 30rpx;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 36rpx;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.logout-area {
|
||||
padding: 16rpx 0 48rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
line-height: 84rpx;
|
||||
background: #ffffff;
|
||||
color: #ef4444;
|
||||
font-size: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.logout-btn::after {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getMembershipPlans, getMyMembership, purchaseMembership } from "@/api/membership";
|
||||
|
||||
const plans = ref([]);
|
||||
const myMembership = ref(null);
|
||||
const loading = ref(true);
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [plansRes, myRes] = await Promise.all([
|
||||
getMembershipPlans(),
|
||||
getMyMembership(),
|
||||
]);
|
||||
plans.value = Array.isArray(plansRes) ? plansRes : [];
|
||||
myMembership.value = myRes || null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePurchase(planId) {
|
||||
uni.showModal({
|
||||
title: "确认开通",
|
||||
content: "确认购买该会员方案?",
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return;
|
||||
try {
|
||||
const res = await purchaseMembership(planId);
|
||||
myMembership.value = res;
|
||||
uni.showToast({ title: "开通成功", icon: "success" });
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || "购买失败", icon: "none" });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return "";
|
||||
const dt = new Date(d);
|
||||
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function daysLeft(endDate) {
|
||||
if (!endDate) return 0;
|
||||
const diff = new Date(endDate).getTime() - Date.now();
|
||||
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
|
||||
}
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="membership-page">
|
||||
<view v-if="loading" class="loading-tip">加载中...</view>
|
||||
<template v-else>
|
||||
<!-- Current membership -->
|
||||
<view v-if="myMembership" class="current-card">
|
||||
<view class="current-header">
|
||||
<uni-icons type="star-filled" size="24" color="#f59e0b" />
|
||||
<text class="current-title">{{ myMembership.plan?.name || '会员' }}</text>
|
||||
</view>
|
||||
<view class="current-info">
|
||||
<view class="current-row">
|
||||
<text class="current-label">到期时间</text>
|
||||
<text class="current-value">{{ formatDate(myMembership.end_date) }}</text>
|
||||
</view>
|
||||
<view class="current-row">
|
||||
<text class="current-label">剩余天数</text>
|
||||
<text class="current-value highlight">{{ daysLeft(myMembership.end_date) }}天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="no-member-card">
|
||||
<uni-icons type="star" size="32" color="#d1d5db" />
|
||||
<text class="no-member-text">您还不是会员</text>
|
||||
</view>
|
||||
|
||||
<!-- Plans -->
|
||||
<view class="section-title">会员方案</view>
|
||||
<view v-if="plans.length === 0" class="empty-tip">暂无可用方案</view>
|
||||
<view v-for="plan in plans" :key="plan.id" class="plan-card">
|
||||
<view class="plan-header">
|
||||
<text class="plan-name">{{ plan.name }}</text>
|
||||
<text class="plan-price">¥{{ plan.price }}</text>
|
||||
</view>
|
||||
<text v-if="plan.description" class="plan-desc">{{ plan.description }}</text>
|
||||
<view class="plan-meta">
|
||||
<text class="plan-duration">{{ plan.duration_days }}天</text>
|
||||
<text v-if="plan.extra_uploads" class="plan-benefit">+{{ plan.extra_uploads }}上传额度</text>
|
||||
<text v-if="plan.extra_top_count" class="plan-benefit">+{{ plan.extra_top_count }}置顶次数</text>
|
||||
</view>
|
||||
<view v-if="plan.benefits" class="plan-benefits">
|
||||
<text class="benefits-text">{{ plan.benefits }}</text>
|
||||
</view>
|
||||
<view class="plan-action">
|
||||
<view class="purchase-btn" @tap="handlePurchase(plan.id)">
|
||||
{{ myMembership ? '续费' : '立即开通' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.membership-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding: 20rpx;
|
||||
}
|
||||
.loading-tip {
|
||||
text-align: center;
|
||||
padding: 100rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.current-card {
|
||||
background: linear-gradient(135deg, #f59e0b, #fbbf24);
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.current-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.current-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.current-info {
|
||||
display: flex;
|
||||
gap: 40rpx;
|
||||
}
|
||||
.current-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.current-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.current-value {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.current-value.highlight {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.no-member-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.no-member-text {
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1e1e2e;
|
||||
margin-bottom: 16rpx;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #9ca3af;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.plan-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
.plan-price {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
}
|
||||
.plan-desc {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.plan-meta {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.plan-duration {
|
||||
font-size: 24rpx;
|
||||
color: #6366f1;
|
||||
background: #eef2ff;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.plan-benefit {
|
||||
font-size: 24rpx;
|
||||
color: #d97706;
|
||||
background: #fef3c7;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.plan-benefits {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.benefits-text {
|
||||
font-size: 24rpx;
|
||||
color: #4b5563;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.plan-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.purchase-btn {
|
||||
padding: 14rpx 40rpx;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,355 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
|
||||
import { getMySpots, deleteSpot } from "@/api/spot";
|
||||
import { extractList } from "@/utils/request";
|
||||
import { resolveImageUrl } from "@/utils/image";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
import { formatSpotPrice } from "@/utils/spot";
|
||||
|
||||
onShow(() => {
|
||||
if (checkLogin()) fetchSpots(true);
|
||||
});
|
||||
|
||||
const spots = ref([]);
|
||||
const page = ref(1);
|
||||
const pageSize = 10;
|
||||
const hasMore = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const statusMap = {
|
||||
pending: { text: "待审核", color: "#f59e0b", bg: "rgba(245,158,11,0.1)" },
|
||||
approved: { text: "已通过", color: "#22c55e", bg: "rgba(34,197,94,0.1)" },
|
||||
rejected: { text: "已拒绝", color: "#ef4444", bg: "rgba(239,68,68,0.1)" },
|
||||
};
|
||||
|
||||
const getStatus = (s) => statusMap[s] || { text: s, color: "#94a3b8", bg: "#f5f6fa" };
|
||||
|
||||
const fetchSpots = async (reset = false) => {
|
||||
if (loading.value) return;
|
||||
if (!reset && !hasMore.value) return;
|
||||
|
||||
loading.value = true;
|
||||
if (reset) {
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMySpots({ page: page.value, page_size: pageSize });
|
||||
const list = extractList(res);
|
||||
|
||||
if (reset) {
|
||||
spots.value = list;
|
||||
} else {
|
||||
spots.value.push(...list);
|
||||
}
|
||||
|
||||
if (list.length < pageSize) {
|
||||
hasMore.value = false;
|
||||
} else {
|
||||
page.value++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goDetail = (id) => {
|
||||
uni.navigateTo({ url: `/pages/spot/detail?id=${id}` });
|
||||
};
|
||||
|
||||
const goEdit = (id) => {
|
||||
uni.navigateTo({ url: `/pages/spot/edit?id=${id}` });
|
||||
};
|
||||
|
||||
const handleDelete = (item) => {
|
||||
uni.showModal({
|
||||
title: "确认删除",
|
||||
content: `确定要删除「${item.title}」吗?此操作不可撤销。`,
|
||||
confirmColor: "#ef4444",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await deleteSpot(item.id);
|
||||
uni.showToast({ title: "已删除", icon: "success" });
|
||||
spots.value = spots.value.filter((s) => s.id !== item.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchSpots(true);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
fetchSpots();
|
||||
});
|
||||
|
||||
fetchSpots(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="my-spots-page">
|
||||
<view class="list">
|
||||
<view
|
||||
v-for="item in spots"
|
||||
:key="item.id"
|
||||
class="spot-card"
|
||||
@tap="goDetail(item.id)"
|
||||
>
|
||||
<image
|
||||
v-if="item.cover_image_url"
|
||||
class="cover"
|
||||
:src="resolveImageUrl(item.cover_image_url)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="cover cover-placeholder">
|
||||
<uni-icons type="camera" size="32" color="#94a3b8" class="placeholder-icon" />
|
||||
</view>
|
||||
<view class="info">
|
||||
<view class="title-row">
|
||||
<text class="title">{{ item.title }}</text>
|
||||
<view
|
||||
class="status-badge"
|
||||
:style="{ background: getStatus(item.audit_status).bg }"
|
||||
>
|
||||
<text
|
||||
class="status-text"
|
||||
:style="{ color: getStatus(item.audit_status).color }"
|
||||
>
|
||||
{{ getStatus(item.audit_status).text }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="meta-row">
|
||||
<view class="city"><uni-icons type="location" size="14" color="#64748b" /> {{ item.city || "未知城市" }}</view>
|
||||
<text
|
||||
class="price-text"
|
||||
:class="{ free: formatSpotPrice(item).isFree, paid: !formatSpotPrice(item).isFree }"
|
||||
>
|
||||
{{ formatSpotPrice(item).label }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="item.audit_status === 'rejected' && item.reject_reason"
|
||||
class="reject-row"
|
||||
>
|
||||
<text class="reject-label">拒绝原因:</text>
|
||||
<text class="reject-reason">{{ item.reject_reason }}</text>
|
||||
</view>
|
||||
<view class="action-row">
|
||||
<view class="action-btn edit-btn" @tap.stop="goEdit(item.id)">
|
||||
<uni-icons type="compose" size="16" color="#6366f1" />
|
||||
<text class="action-text edit-text">编辑</text>
|
||||
</view>
|
||||
<view class="action-btn delete-btn" @tap.stop="handleDelete(item)">
|
||||
<uni-icons type="trash" size="16" color="#ef4444" />
|
||||
<text class="action-text delete-text">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="status-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!hasMore && spots.length > 0" class="status-tip">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
<view v-else-if="!loading && spots.length === 0" class="empty-state">
|
||||
<uni-icons type="location" size="48" color="#6366f1" class="empty-icon" />
|
||||
<text class="empty-title">还没有投稿地点</text>
|
||||
<text class="empty-desc">去投稿你发现的取景地吧</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.my-spots-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.spot-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
}
|
||||
|
||||
.cover-placeholder {
|
||||
background: #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 64rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 20rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.city {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-text.free {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.price-text.paid {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.reject-row {
|
||||
margin-top: 12rpx;
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
padding: 16rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.reject-label {
|
||||
font-size: 24rpx;
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reject-reason {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.edit-text {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.delete-text {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-tip {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #94a3b8;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 160rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
|
||||
import { getNotifications, markAllRead, markRead } from "@/api/notification";
|
||||
import { extractList } from "@/utils/request";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
|
||||
onShow(() => { checkLogin(); });
|
||||
|
||||
const items = ref([]);
|
||||
const page = ref(1);
|
||||
const pageSize = 20;
|
||||
const hasMore = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const typeIcon = {
|
||||
audit: "checkbox-filled",
|
||||
comment: "chat",
|
||||
system: "info",
|
||||
};
|
||||
const typeColor = {
|
||||
audit: "#6366f1",
|
||||
comment: "#3b82f6",
|
||||
system: "#94a3b8",
|
||||
};
|
||||
|
||||
const fetchList = async (reset = false) => {
|
||||
if (loading.value) return;
|
||||
if (!reset && !hasMore.value) return;
|
||||
loading.value = true;
|
||||
if (reset) { page.value = 1; hasMore.value = true; }
|
||||
try {
|
||||
const res = await getNotifications({ page: page.value, page_size: pageSize });
|
||||
const list = extractList(res);
|
||||
if (reset) items.value = list; else items.value.push(...list);
|
||||
if (list.length < pageSize) hasMore.value = false; else page.value++;
|
||||
} catch (e) { console.error(e); }
|
||||
finally { loading.value = false; }
|
||||
};
|
||||
|
||||
const handleTap = async (item) => {
|
||||
if (!item.is_read) {
|
||||
try { await markRead(item.id); item.is_read = true; } catch (e) { /* */ }
|
||||
}
|
||||
if (item.ref_type === "spot" && item.ref_id) {
|
||||
uni.navigateTo({ url: `/pages/spot/detail?id=${item.ref_id}` });
|
||||
}
|
||||
};
|
||||
|
||||
const handleReadAll = async () => {
|
||||
try {
|
||||
await markAllRead();
|
||||
items.value.forEach((n) => (n.is_read = true));
|
||||
uni.showToast({ title: "已全部标记已读", icon: "success" });
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
onPullDownRefresh(async () => { await fetchList(true); uni.stopPullDownRefresh(); });
|
||||
onReachBottom(() => { fetchList(); });
|
||||
|
||||
fetchList(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="notification-page">
|
||||
<view class="header-bar">
|
||||
<text class="header-title">消息通知</text>
|
||||
<text class="read-all" @tap="handleReadAll">全部已读</text>
|
||||
</view>
|
||||
|
||||
<view class="list">
|
||||
<view
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="noti-item"
|
||||
:class="{ unread: !item.is_read }"
|
||||
@tap="handleTap(item)"
|
||||
>
|
||||
<view class="noti-icon" :style="{ background: (typeColor[item.type] || '#94a3b8') + '18' }">
|
||||
<uni-icons :type="typeIcon[item.type] || 'info'" size="20" :color="typeColor[item.type] || '#94a3b8'" />
|
||||
</view>
|
||||
<view class="noti-body">
|
||||
<text class="noti-title">{{ item.title }}</text>
|
||||
<text v-if="item.content" class="noti-content">{{ item.content }}</text>
|
||||
<text class="noti-time">{{ item.created_at?.slice(0, 16).replace('T', ' ') }}</text>
|
||||
</view>
|
||||
<view v-if="!item.is_read" class="noti-dot" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="status-tip"><text>加载中...</text></view>
|
||||
<view v-else-if="!hasMore && items.length > 0" class="status-tip"><text>没有更多了</text></view>
|
||||
<view v-else-if="!loading && items.length === 0" class="empty-state">
|
||||
<uni-icons type="chat" size="48" color="#cbd5e1" />
|
||||
<text class="empty-text">暂无消息</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notification-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
.header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
.read-all {
|
||||
font-size: 26rpx;
|
||||
color: #6366f1;
|
||||
}
|
||||
.list {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.noti-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
}
|
||||
.noti-item.unread {
|
||||
background: #f0f0ff;
|
||||
}
|
||||
.noti-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.noti-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.noti-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.noti-content {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.noti-time {
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.noti-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background: #ef4444;
|
||||
border-radius: 8rpx;
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
right: 24rpx;
|
||||
}
|
||||
.status-tip {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #94a3b8;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 160rpx 0;
|
||||
}
|
||||
.empty-text {
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,247 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
|
||||
import { getMyPoints, getMyPointRecords } from "@/api/point";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
|
||||
onShow(() => { checkLogin(); });
|
||||
|
||||
const balance = ref(0);
|
||||
const records = ref([]);
|
||||
const page = ref(1);
|
||||
const pageSize = 15;
|
||||
const hasMore = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchBalance = async () => {
|
||||
try {
|
||||
const res = await getMyPoints();
|
||||
balance.value = res.balance ?? res.points ?? 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecords = async (reset = false) => {
|
||||
if (loading.value) return;
|
||||
if (!reset && !hasMore.value) return;
|
||||
|
||||
loading.value = true;
|
||||
if (reset) {
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMyPointRecords({ page: page.value, page_size: pageSize });
|
||||
const list = res.items || res.data || res || [];
|
||||
|
||||
if (reset) {
|
||||
records.value = list;
|
||||
} else {
|
||||
records.value.push(...list);
|
||||
}
|
||||
|
||||
if (list.length < pageSize) {
|
||||
hasMore.value = false;
|
||||
} else {
|
||||
page.value++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return "";
|
||||
const d = new Date(dateStr);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
const h = String(d.getHours()).padStart(2, "0");
|
||||
const min = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${y}-${m}-${day} ${h}:${min}`;
|
||||
};
|
||||
|
||||
const formatChange = (val) => {
|
||||
return val > 0 ? `+${val}` : `${val}`;
|
||||
};
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await Promise.all([fetchBalance(), fetchRecords(true)]);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
fetchRecords();
|
||||
});
|
||||
|
||||
fetchBalance();
|
||||
fetchRecords(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="points-page">
|
||||
<view class="balance-card">
|
||||
<text class="balance-label">当前积分</text>
|
||||
<text class="balance-value">{{ balance }}</text>
|
||||
</view>
|
||||
|
||||
<view class="section-header">
|
||||
<text class="section-title">积分记录</text>
|
||||
</view>
|
||||
|
||||
<view class="records-list">
|
||||
<view
|
||||
v-for="item in records"
|
||||
:key="item.id"
|
||||
class="record-item"
|
||||
>
|
||||
<view class="record-left">
|
||||
<text class="record-reason">{{ item.reason || "积分变动" }}</text>
|
||||
<text class="record-date">{{ formatDate(item.created_at) }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="record-change"
|
||||
:class="item.change > 0 ? 'positive' : 'negative'"
|
||||
>
|
||||
{{ formatChange(item.change) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="status-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!hasMore && records.length > 0" class="status-tip">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
<view v-else-if="!loading && records.length === 0" class="empty-state">
|
||||
<uni-icons type="star-filled" size="48" color="#6366f1" class="empty-icon" />
|
||||
<text class="empty-title">暂无积分记录</text>
|
||||
<text class="empty-desc">投稿取景地、获得收藏可以赚取积分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.points-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
margin: 24rpx 32rpx;
|
||||
background: linear-gradient(135deg, #6366f1, #818cf8);
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 80rpx;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 24rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #ffffff;
|
||||
padding: 28rpx 28rpx;
|
||||
border-radius: 14rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.record-left {
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.record-reason {
|
||||
font-size: 28rpx;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.record-date {
|
||||
font-size: 24rpx;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.record-change {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-change.positive {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.record-change.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-tip {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #94a3b8;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,350 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { updateMyInfo } from "@/api/user";
|
||||
import { uploadImage } from "@/api/spot";
|
||||
import { resolveImageUrl } from "@/utils/image";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
import cityData from "@/utils/city-data";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const loading = ref(false);
|
||||
|
||||
const form = ref({
|
||||
nickname: "",
|
||||
avatar_url: "",
|
||||
city: "",
|
||||
bio: "",
|
||||
identity: "both",
|
||||
});
|
||||
|
||||
const identityOptions = [
|
||||
{ label: "摄影师", value: "photographer" },
|
||||
{ label: "Coser", value: "cosplayer" },
|
||||
{ label: "都是", value: "both" },
|
||||
];
|
||||
|
||||
const provinces = cityData.map((p) => p.province);
|
||||
const cityColumns = ref([provinces, cityData[0].cities]);
|
||||
const cityPickerIndex = ref([0, 0]);
|
||||
|
||||
const initForm = () => {
|
||||
const u = userStore.userInfo;
|
||||
if (!u) return;
|
||||
form.value.nickname = u.nickname || "";
|
||||
form.value.avatar_url = u.avatar_url || "";
|
||||
form.value.bio = u.bio || "";
|
||||
form.value.identity = u.identity || "both";
|
||||
form.value.city = u.city || "";
|
||||
|
||||
if (u.city) {
|
||||
const parts = u.city.split(" ");
|
||||
if (parts.length === 2) {
|
||||
const pi = provinces.indexOf(parts[0]);
|
||||
if (pi >= 0) {
|
||||
const ci = cityData[pi].cities.indexOf(parts[1]);
|
||||
cityPickerIndex.value = [pi, ci >= 0 ? ci : 0];
|
||||
cityColumns.value = [provinces, cityData[pi].cities];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
if (!checkLogin()) return;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.fetchUserInfo();
|
||||
initForm();
|
||||
});
|
||||
|
||||
const onCityColumnChange = (e) => {
|
||||
const { column, value } = e.detail;
|
||||
if (column === 0) {
|
||||
cityColumns.value = [provinces, cityData[value].cities];
|
||||
cityPickerIndex.value = [value, 0];
|
||||
} else {
|
||||
cityPickerIndex.value = [cityPickerIndex.value[0], value];
|
||||
}
|
||||
};
|
||||
|
||||
const onCityChange = (e) => {
|
||||
const [pi, ci] = e.detail.value;
|
||||
const province = provinces[pi];
|
||||
const city = cityData[pi].cities[ci];
|
||||
form.value.city = `${province} ${city}`;
|
||||
};
|
||||
|
||||
const chooseAvatar = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ["compressed"],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFilePaths[0];
|
||||
try {
|
||||
uni.showLoading({ title: "上传中..." });
|
||||
const data = await uploadImage(tempPath);
|
||||
form.value.avatar_url = data.url;
|
||||
uni.hideLoading();
|
||||
} catch (e) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "头像上传失败", icon: "none" });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.value.nickname.trim()) {
|
||||
uni.showToast({ title: "昵称不能为空", icon: "none" });
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
await updateMyInfo({
|
||||
nickname: form.value.nickname.trim(),
|
||||
avatar_url: form.value.avatar_url || null,
|
||||
city: form.value.city || null,
|
||||
bio: form.value.bio.trim() || null,
|
||||
identity: form.value.identity,
|
||||
});
|
||||
await userStore.fetchUserInfo();
|
||||
uni.showToast({ title: "保存成功", icon: "success" });
|
||||
setTimeout(() => uni.navigateBack(), 800);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<view class="avatar-section" @tap="chooseAvatar">
|
||||
<image
|
||||
v-if="form.avatar_url"
|
||||
class="avatar-img"
|
||||
:src="resolveImageUrl(form.avatar_url)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar-img avatar-placeholder">
|
||||
<uni-icons type="camera-filled" size="32" color="#94a3b8" />
|
||||
</view>
|
||||
<text class="avatar-tip">点击更换头像</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="field">
|
||||
<text class="label">昵称</text>
|
||||
<input
|
||||
v-model="form.nickname"
|
||||
class="input"
|
||||
maxlength="20"
|
||||
placeholder="请输入昵称"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">个人简介</text>
|
||||
<textarea
|
||||
v-model="form.bio"
|
||||
class="textarea"
|
||||
maxlength="120"
|
||||
placeholder="一句话介绍自己"
|
||||
placeholder-class="placeholder"
|
||||
:auto-height="false"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">所在城市</text>
|
||||
<picker
|
||||
mode="multiSelector"
|
||||
:value="cityPickerIndex"
|
||||
:range="cityColumns"
|
||||
@columnchange="onCityColumnChange"
|
||||
@change="onCityChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
<text :class="form.city ? '' : 'placeholder'">{{ form.city || '请选择城市' }}</text>
|
||||
<uni-icons type="right" size="16" color="#cbd5e1" />
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">身份</text>
|
||||
<view class="identity-toggle">
|
||||
<view
|
||||
v-for="opt in identityOptions"
|
||||
:key="opt.value"
|
||||
class="toggle-btn"
|
||||
:class="{ active: form.identity === opt.value }"
|
||||
@tap="form.identity = opt.value"
|
||||
>
|
||||
<text class="toggle-text">{{ opt.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="save-btn" :loading="loading" :disabled="loading" @tap="handleSave">
|
||||
保存
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 80rpx;
|
||||
border: 4rpx solid #e2e8f0;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
background: #f1f5f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-tip {
|
||||
font-size: 24rpx;
|
||||
color: #6366f1;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #64748b;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #f8fafc;
|
||||
border: 2rpx solid #e2e8f0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1e293b;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
background: #f8fafc;
|
||||
border: 2rpx solid #e2e8f0;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1e293b;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 80rpx;
|
||||
background: #f8fafc;
|
||||
border: 2rpx solid #e2e8f0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.identity-toggle {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f6fa;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx solid #e2e8f0;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
font-size: 28rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.toggle-btn.active .toggle-text {
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #6366f1;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.save-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.save-btn[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
import { changePassword } from "@/api/user";
|
||||
import { useUserStore } from "@/store/user";
|
||||
|
||||
onShow(() => { checkLogin(); });
|
||||
|
||||
const userStore = useUserStore();
|
||||
const showPwdForm = ref(false);
|
||||
const pwdForm = ref({ old_password: "", new_password: "", confirm: "" });
|
||||
const saving = ref(false);
|
||||
|
||||
const handleChangePwd = async () => {
|
||||
if (!pwdForm.value.old_password || !pwdForm.value.new_password) {
|
||||
uni.showToast({ title: "请填写完整", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (pwdForm.value.new_password.length < 6) {
|
||||
uni.showToast({ title: "新密码至少6位", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (pwdForm.value.new_password !== pwdForm.value.confirm) {
|
||||
uni.showToast({ title: "两次密码不一致", icon: "none" });
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await changePassword({
|
||||
old_password: pwdForm.value.old_password,
|
||||
new_password: pwdForm.value.new_password,
|
||||
});
|
||||
uni.showToast({ title: "密码修改成功", icon: "success" });
|
||||
showPwdForm.value = false;
|
||||
pwdForm.value = { old_password: "", new_password: "", confirm: "" };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearCache = () => {
|
||||
uni.showModal({
|
||||
title: "清除缓存",
|
||||
content: "将清除本地缓存数据(不会影响账号数据),确认继续?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const token = uni.getStorageSync("access_token");
|
||||
const rt = uni.getStorageSync("refresh_token");
|
||||
uni.clearStorageSync();
|
||||
if (token) uni.setStorageSync("access_token", token);
|
||||
if (rt) uni.setStorageSync("refresh_token", rt);
|
||||
uni.showToast({ title: "缓存已清除", icon: "success" });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) userStore.logout();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="settings-page">
|
||||
<view class="menu-card">
|
||||
<view class="menu-item" @tap="showPwdForm = !showPwdForm">
|
||||
<view class="menu-left">
|
||||
<uni-icons type="locked" size="22" color="#6366f1" />
|
||||
<text class="menu-label">修改密码</text>
|
||||
</view>
|
||||
<uni-icons :type="showPwdForm ? 'arrowup' : 'right'" size="16" color="#cbd5e1" />
|
||||
</view>
|
||||
|
||||
<view v-if="showPwdForm" class="pwd-form">
|
||||
<input
|
||||
v-model="pwdForm.old_password"
|
||||
class="pwd-input"
|
||||
type="password"
|
||||
placeholder="当前密码"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<input
|
||||
v-model="pwdForm.new_password"
|
||||
class="pwd-input"
|
||||
type="password"
|
||||
placeholder="新密码(至少6位)"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<input
|
||||
v-model="pwdForm.confirm"
|
||||
class="pwd-input"
|
||||
type="password"
|
||||
placeholder="确认新密码"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<button class="pwd-btn" :loading="saving" :disabled="saving" @tap="handleChangePwd">
|
||||
确认修改
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @tap="clearCache">
|
||||
<view class="menu-left">
|
||||
<uni-icons type="trash" size="22" color="#f59e0b" />
|
||||
<text class="menu-label">清除缓存</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#cbd5e1" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="about-card">
|
||||
<text class="about-title">关于次元取景器</text>
|
||||
<text class="about-desc">发现和分享二次元取景地的社区平台</text>
|
||||
<text class="about-version">v1.0.0</text>
|
||||
</view>
|
||||
|
||||
<view class="logout-area">
|
||||
<button class="logout-btn" @tap="handleLogout">退出登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
.menu-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
}
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.menu-label {
|
||||
font-size: 30rpx;
|
||||
color: #1e293b;
|
||||
}
|
||||
.pwd-form {
|
||||
padding: 20rpx 32rpx 28rpx;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.pwd-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid #e2e8f0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1e293b;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.placeholder { color: #94a3b8; }
|
||||
.pwd-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.pwd-btn::after { border: none; }
|
||||
.pwd-btn[disabled] { opacity: 0.6; }
|
||||
.about-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.about-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.about-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #64748b;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.about-version {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.logout-area { padding: 16rpx 0 48rpx; }
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
line-height: 84rpx;
|
||||
background: #fff;
|
||||
color: #ef4444;
|
||||
font-size: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.logout-btn::after { border: none; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user