feat: 我的、设置、打印指南页面开发完成
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"name": "supportPages",
|
||||
"pages": [
|
||||
"guide/guide",
|
||||
"settings/settings",
|
||||
"debug/debug",
|
||||
"worksheetSync/worksheetSync",
|
||||
"categoryManage/categoryManage",
|
||||
|
||||
+8
-3
@@ -12,6 +12,7 @@ App<IAppOption>({
|
||||
globalData: {
|
||||
env: 'release',
|
||||
uuid: '',
|
||||
user: null,
|
||||
printConfig: defaultPrintConfig,
|
||||
pageConfig: undefined,
|
||||
pageConfigReady: false,
|
||||
@@ -41,9 +42,13 @@ App<IAppOption>({
|
||||
}
|
||||
|
||||
// 静默登录(不阻塞页面渲染)
|
||||
getUser().catch((err) => {
|
||||
console.error('静默登录失败', err);
|
||||
});
|
||||
getUser()
|
||||
.then((user) => {
|
||||
this.globalData.user = user;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('静默登录失败', err);
|
||||
});
|
||||
|
||||
void this.loadPageConfig();
|
||||
},
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "47510723",
|
||||
"name": "iconify-tabler_mood-empty",
|
||||
"font_class": "iconify-tabler_mood-empty",
|
||||
"unicode": "e62f",
|
||||
"unicode_decimal": 58927
|
||||
},
|
||||
{
|
||||
"icon_id": "47501281",
|
||||
"name": "loading",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -18,6 +18,8 @@ export type CategoryItem = {
|
||||
downloads: number;
|
||||
/** 列表底部展示的日期(静态数据用 id 派生稳定值) */
|
||||
date: string;
|
||||
/** 最后更新时间(云端数据用于排序) */
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type CategoryGroup = {
|
||||
|
||||
@@ -17,6 +17,13 @@ type CategoryDataset = {
|
||||
}>;
|
||||
};
|
||||
|
||||
const DIFFICULTY_LABELS: Record<number, string> = {
|
||||
1: '入门',
|
||||
2: '基础',
|
||||
3: '进阶',
|
||||
4: '挑战',
|
||||
};
|
||||
|
||||
const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(
|
||||
({ id, name, icon }) => ({
|
||||
id,
|
||||
@@ -33,9 +40,74 @@ const TAB_BAR_PATHS = new Set([
|
||||
'/pages/profile/profile',
|
||||
]);
|
||||
|
||||
// Page-level mutable state (shared across methods, set once on load)
|
||||
// Page-level mutable state (shared across methods)
|
||||
let _categoryData: CategoryDataset | null = null;
|
||||
|
||||
/** 将云端 worksheet 原始数据转换为 CategoryItem */
|
||||
function toDisplayItem(raw: Record<string, any>): CategoryItem {
|
||||
const difficulty = (Number(raw.difficulty) || 2) as 1 | 2 | 3 | 4;
|
||||
const ageMin = Number(raw.ageMin) || 0;
|
||||
const ageMax = Number(raw.ageMax) || 0;
|
||||
return {
|
||||
id: String(raw._id || ''),
|
||||
title: String(raw.title || ''),
|
||||
subtitle: String(raw.subtitle || ''),
|
||||
previewImg: String(raw.previewImg || ''),
|
||||
ageBand: `${ageMin}-${ageMax}岁`,
|
||||
ageMin,
|
||||
ageMax,
|
||||
difficulty,
|
||||
difficultyLabel: DIFFICULTY_LABELS[difficulty] || '基础',
|
||||
path: String(raw.path || ''),
|
||||
available: true,
|
||||
likes: Number(raw.likes) || 0,
|
||||
downloads: Number(raw.downloads) || 0,
|
||||
date: raw.updatedAt
|
||||
? new Date(raw.updatedAt).toISOString().slice(0, 10)
|
||||
: new Date().toISOString().slice(0, 10),
|
||||
updatedAt: raw.updatedAt ? String(raw.updatedAt) : '',
|
||||
};
|
||||
}
|
||||
|
||||
/** 排序:updatedAt 最新的 2 个置顶,其余按 downloads 降序 */
|
||||
function sortWorksheets(items: CategoryItem[]): CategoryItem[] {
|
||||
const byTime = [...items].sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt || '').getTime() -
|
||||
new Date(a.updatedAt || '').getTime(),
|
||||
);
|
||||
const recent = byTime.slice(0, 2);
|
||||
const recentIds = new Set(recent.map((w) => w.id));
|
||||
const rest = byTime
|
||||
.filter((w) => !recentIds.has(w.id))
|
||||
.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
||||
return [...recent, ...rest];
|
||||
}
|
||||
|
||||
/** 将云端 worksheet 数组按分类分组,构建 CategoryDataset */
|
||||
function buildCategoryDataFromCloud(
|
||||
rawList: Record<string, any>[],
|
||||
): CategoryDataset {
|
||||
const itemsByCategory: Record<string, CategoryItem[]> = {};
|
||||
for (const raw of rawList) {
|
||||
const item = toDisplayItem(raw);
|
||||
const cat = String(raw.category || '');
|
||||
if (!itemsByCategory[cat]) itemsByCategory[cat] = [];
|
||||
itemsByCategory[cat].push(item);
|
||||
}
|
||||
|
||||
const categories = CATEGORY_LIST_WITH_ALL.filter((c) => c.id !== 'all').map(
|
||||
(cat) => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
icon: cat.icon,
|
||||
items: sortWorksheets(itemsByCategory[cat.id] || []),
|
||||
}),
|
||||
);
|
||||
|
||||
return { searchPlaceholder: '搜索练习纸...', categories };
|
||||
}
|
||||
|
||||
function buildAllItems(): CategoryItem[] {
|
||||
if (!_categoryData) return [];
|
||||
const items: CategoryItem[] = [];
|
||||
@@ -44,7 +116,7 @@ function buildAllItems(): CategoryItem[] {
|
||||
items.push({ ...item });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
return sortWorksheets(items);
|
||||
}
|
||||
|
||||
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
||||
@@ -69,7 +141,7 @@ function filterByKeyword(
|
||||
}
|
||||
|
||||
// Skeleton placeholder items for loading state
|
||||
const SKELETON_ITEMS = Array.from({ length: 4 }, (_, i) => ({
|
||||
const SKELETON_ITEMS = Array.from({ length: 6 }, (_, i) => ({
|
||||
id: `skeleton-${i}`,
|
||||
title: '\u00A0',
|
||||
subtitle: '\u00A0',
|
||||
@@ -87,45 +159,45 @@ Page({
|
||||
loading: true,
|
||||
},
|
||||
|
||||
_defaultCategoryId: 'all',
|
||||
|
||||
onLoad(options: Record<string, string>) {
|
||||
void this.initData(options);
|
||||
if (options.id && options.id !== 'all') {
|
||||
this._defaultCategoryId = options.id;
|
||||
this.setData({ activeCategoryId: options.id });
|
||||
}
|
||||
},
|
||||
|
||||
async initData(options: Record<string, string>) {
|
||||
wx.showLoading({ title: '加载中', mask: false });
|
||||
onShow() {
|
||||
_categoryData = null;
|
||||
this.setData({ displayItems: SKELETON_ITEMS, loading: true });
|
||||
void this.loadWorksheets();
|
||||
},
|
||||
|
||||
const app = getApp<IAppOption>();
|
||||
const config = await app.getPageConfig();
|
||||
async loadWorksheets() {
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'worksheetsQuery',
|
||||
data: { status: 'active' },
|
||||
});
|
||||
const result = (res?.result as any) || {};
|
||||
if (!result.success) throw new Error(result.message || '查询失败');
|
||||
|
||||
// Use cloud config if available, otherwise fallback to local static data
|
||||
if (config?.category?.categories?.length) {
|
||||
_categoryData = {
|
||||
searchPlaceholder:
|
||||
config.category.searchPlaceholder || '搜索练习纸...',
|
||||
categories: config.category.categories.map((cat) => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
icon: cat.icon,
|
||||
items: (cat.items || []).map(
|
||||
(raw) => raw as unknown as CategoryItem,
|
||||
),
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
_categoryData = buildCategoryDataFromCloud(result.data || []);
|
||||
} catch {
|
||||
_categoryData = CATEGORY_DATA;
|
||||
}
|
||||
|
||||
const activeId = options.id && options.id !== 'all' ? options.id : 'all';
|
||||
const activeId = this.data.activeCategoryId || this._defaultCategoryId;
|
||||
const items = getItemsByCategory(activeId);
|
||||
const filtered = filterByKeyword(items, this.data.searchKeyword);
|
||||
|
||||
this.setData({
|
||||
loading: false,
|
||||
searchPlaceholder: _categoryData.searchPlaceholder,
|
||||
activeCategoryId: activeId,
|
||||
displayItems: items,
|
||||
displayItems: filtered,
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
},
|
||||
|
||||
onTapCategory(e: WechatMiniprogram.TouchEvent) {
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
|
||||
.fav-cta__btn {
|
||||
margin-top: 32rpx;
|
||||
padding: 22rpx 64rpx;
|
||||
padding: 24rpx 52rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
@@ -425,7 +425,7 @@
|
||||
.fav-empty__sub {
|
||||
font-size: 26rpx;
|
||||
color: @text-secondary;
|
||||
margin-bottom: 40rpx;
|
||||
margin-bottom: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,15 @@
|
||||
aria-hidden="true"></text>
|
||||
</view>
|
||||
<view wx:elif="{{favoriteList.length === 0}}" class="fav-empty">
|
||||
<text class="fav-empty__emoji">🦆</text>
|
||||
<text class="fav-empty__msg">鸭丫还没找到收藏呢</text>
|
||||
<toy-icon
|
||||
name="mood-empty"
|
||||
size="100rpx"
|
||||
color="#b3ac9f"
|
||||
class="fav-empty__icon" />
|
||||
<text class="fav-empty__msg">还没找到收藏呢</text>
|
||||
<text class="fav-empty__sub">去发现好玩的练习纸吧~</text>
|
||||
<button class="fav-empty__btn" bindtap="onDiscoverTap">
|
||||
去发现
|
||||
<button class="fav-cta__btn" bindtap="onDiscoverTap">
|
||||
去发现页看看
|
||||
</button>
|
||||
</view>
|
||||
<view wx:elif="{{favoriteList.length > 0}}" class="fav-list">
|
||||
@@ -112,14 +116,15 @@
|
||||
<view
|
||||
wx:elif="{{downloadSections.length === 0}}"
|
||||
class="fav-empty">
|
||||
<van-icon
|
||||
name="clock"
|
||||
size="120rpx"
|
||||
<toy-icon
|
||||
name="mood-empty"
|
||||
size="100rpx"
|
||||
color="#b3ac9f"
|
||||
class="fav-empty__icon" />
|
||||
<text class="fav-empty__msg">暂无下载记录</text>
|
||||
<button class="fav-empty__btn" bindtap="onDiscoverTap">
|
||||
去发现
|
||||
<text class="fav-empty__sub">去发现好玩的练习纸吧~</text>
|
||||
<button class="fav-cta__btn" bindtap="onDiscoverTap">
|
||||
去发现页看看
|
||||
</button>
|
||||
</view>
|
||||
<view wx:elif="{{downloadSections.length > 0}}" class="fav-dl">
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
|
||||
<swiper
|
||||
class="home-hero-swiper"
|
||||
previous-margin="100rpx"
|
||||
next-margin="100rpx"
|
||||
previous-margin="80rpx"
|
||||
next-margin="80rpx"
|
||||
circular="{{featuredItems.length > 1 && !loading}}"
|
||||
autoplay="{{!loading}}"
|
||||
interval="{{4000}}"
|
||||
@@ -200,8 +200,7 @@
|
||||
path="{{item.path}}"
|
||||
data-title="{{item.title}}"
|
||||
data-path="{{item.path}}"
|
||||
bindtap="onTapCard"
|
||||
/>
|
||||
bindtap="onTapCard" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"backgroundColor": "#F8F0E0",
|
||||
"usingComponents": {
|
||||
"van-icon": "@vant/weapp/icon/index",
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar"
|
||||
"van-popup": "@vant/weapp/popup/index",
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||
"toy-icon": "../../toy/icon/icon"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ page {
|
||||
}
|
||||
|
||||
.profile-hero__avatar {
|
||||
width: 256rpx;
|
||||
height: 256rpx;
|
||||
width: 224rpx;
|
||||
height: 224rpx;
|
||||
border-radius: 50%;
|
||||
border: 8rpx solid @bg-white;
|
||||
background: @bg-gray;
|
||||
@@ -98,7 +98,7 @@ page {
|
||||
|
||||
.profile-hero__name {
|
||||
margin-top: 32rpx;
|
||||
font-size: 40rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
text-align: center;
|
||||
@@ -116,7 +116,7 @@ page {
|
||||
.profile-hero__stat {
|
||||
flex: 1;
|
||||
background: @bg-header;
|
||||
border-radius: @radius;
|
||||
border-radius: 48rpx;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
padding: 32rpx 24rpx;
|
||||
text-align: center;
|
||||
@@ -321,4 +321,144 @@ page {
|
||||
color: @text-secondary;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.profile-editor-popup {
|
||||
border-radius: 40rpx 40rpx 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-editor {
|
||||
overflow-y: auto;
|
||||
padding: 32rpx 40rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: @bg-white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-editor__header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.profile-editor__title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.profile-editor__desc {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
.profile-editor__avatar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.profile-editor__avatar-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.profile-editor__avatar-button::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.profile-editor__avatar {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 50%;
|
||||
background: @bg-gray;
|
||||
border: 2rpx dashed fade(@text-secondary, 30%);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-editor__avatar-placeholder {
|
||||
width: 120rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
color: @text-secondary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-editor__avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.profile-editor__field {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.profile-editor__label {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.profile-editor__input {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 24rpx;
|
||||
background: @bg-header;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
box-sizing: border-box;
|
||||
font-size: 30rpx;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.profile-editor__actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 24rpx;
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
.profile-editor__action {
|
||||
height: 92rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-editor__action--ghost {
|
||||
background: @bg-header;
|
||||
color: @text-title;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
}
|
||||
|
||||
.profile-editor__action--primary {
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.profile-editor__action--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -1,31 +1,154 @@
|
||||
import { getTodayDownloadCount } from '../../utils/downloadPrint';
|
||||
import { getDownloadLogCount } from '../../utils/downloadLogs';
|
||||
import { getFavoriteCount } from '../../utils/favorites';
|
||||
import {
|
||||
getCachedUser,
|
||||
getUser,
|
||||
type UserInfo,
|
||||
updateUserProfile,
|
||||
} from '../../utils/auth';
|
||||
|
||||
Page({
|
||||
DEFAULT_AVATAR_URL: '/assets/imgs/doodle-head.png',
|
||||
data: {
|
||||
userName: '微信用户',
|
||||
avatarUrl: '',
|
||||
printCount: 0,
|
||||
favoriteCount: 0,
|
||||
version: '',
|
||||
isDevEnv: false,
|
||||
version: '3.0.0',
|
||||
showProfileEditor: false,
|
||||
draftNickName: '',
|
||||
draftAvatarUrl: '/assets/imgs/doodle-head.png',
|
||||
isSavingProfile: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getAccountInfoSync();
|
||||
const version = info.miniProgram.version || '开发版';
|
||||
const isDevEnv = info.miniProgram.envVersion === 'develop';
|
||||
this.setData({ version, isDevEnv });
|
||||
this.syncUserInfoFromApp();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.refreshStats();
|
||||
this.syncUserInfoFromApp();
|
||||
void this.refreshStats();
|
||||
},
|
||||
|
||||
refreshStats() {
|
||||
const printCount = getTodayDownloadCount();
|
||||
// 收藏数待与收藏页数据源打通后接入
|
||||
const favoriteCount = 0;
|
||||
this.setData({ printCount, favoriteCount });
|
||||
async syncUserInfoFromApp() {
|
||||
const app = getApp<IAppOption>();
|
||||
const cachedUser = app.globalData.user || getCachedUser();
|
||||
|
||||
if (cachedUser) {
|
||||
this.applyUserInfo(cachedUser);
|
||||
}
|
||||
|
||||
try {
|
||||
const latestUser = await getUser();
|
||||
this.applyUserInfo(latestUser);
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败', error);
|
||||
}
|
||||
},
|
||||
|
||||
applyUserInfo(user: UserInfo) {
|
||||
const userName = user.nickName?.trim() || '微信用户';
|
||||
const avatarUrl =
|
||||
user.avatarUrl || (this as any).DEFAULT_AVATAR_URL || '';
|
||||
this.setData({
|
||||
userName,
|
||||
avatarUrl,
|
||||
draftNickName: userName === '微信用户' ? '' : userName,
|
||||
draftAvatarUrl: avatarUrl,
|
||||
});
|
||||
},
|
||||
|
||||
async refreshStats() {
|
||||
try {
|
||||
const [printCount, favoriteCount] = await Promise.all([
|
||||
getDownloadLogCount(),
|
||||
getFavoriteCount(),
|
||||
]);
|
||||
this.setData({ printCount, favoriteCount });
|
||||
} catch (e) {
|
||||
console.error('refreshStats failed', e);
|
||||
}
|
||||
},
|
||||
|
||||
onOpenProfileEditor() {
|
||||
this.setData({
|
||||
showProfileEditor: true,
|
||||
draftNickName:
|
||||
this.data.userName === '微信用户' ? '' : this.data.userName,
|
||||
draftAvatarUrl: this.data.avatarUrl,
|
||||
});
|
||||
},
|
||||
|
||||
onCloseProfileEditor() {
|
||||
if (this.data.isSavingProfile) return;
|
||||
this.setData({ showProfileEditor: false });
|
||||
},
|
||||
|
||||
onChooseAvatar(e: WechatMiniprogram.CustomEvent<{ avatarUrl?: string }>) {
|
||||
const avatarUrl = e.detail?.avatarUrl || '';
|
||||
if (!avatarUrl) return;
|
||||
this.setData({ draftAvatarUrl: avatarUrl });
|
||||
},
|
||||
|
||||
onNicknameInput(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
const nickName = (e.detail?.value || '').trim();
|
||||
this.setData({ draftNickName: nickName });
|
||||
},
|
||||
|
||||
async onSubmitProfile() {
|
||||
if (this.data.isSavingProfile) return;
|
||||
|
||||
const nickName = this.data.draftNickName.trim();
|
||||
if (!nickName) {
|
||||
wx.showToast({ title: '请输入昵称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.data.draftAvatarUrl) {
|
||||
wx.showToast({ title: '请选择头像', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setData({ isSavingProfile: true });
|
||||
|
||||
try {
|
||||
const avatarUrl = await this.persistAvatarIfNeeded(
|
||||
this.data.draftAvatarUrl,
|
||||
);
|
||||
const user = await updateUserProfile({
|
||||
nickName,
|
||||
avatarUrl,
|
||||
});
|
||||
|
||||
this.applyUserInfo(user);
|
||||
this.setData({ showProfileEditor: false });
|
||||
wx.showToast({ title: '资料已更新', icon: 'success' });
|
||||
} catch (error) {
|
||||
console.error('更新用户资料失败', error);
|
||||
wx.showToast({ title: '更新失败,请稍后重试', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ isSavingProfile: false });
|
||||
}
|
||||
},
|
||||
|
||||
async persistAvatarIfNeeded(avatarUrl: string): Promise<string> {
|
||||
if (!avatarUrl) return avatarUrl;
|
||||
if (
|
||||
avatarUrl.startsWith('cloud://') ||
|
||||
avatarUrl.startsWith('http://') ||
|
||||
avatarUrl.startsWith('https://')
|
||||
) {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
const user = await getUser();
|
||||
const cloudPath = `user-avatars/${user._id}-${Date.now()}.png`;
|
||||
const uploadRes = await wx.cloud.uploadFile({
|
||||
cloudPath,
|
||||
filePath: avatarUrl,
|
||||
});
|
||||
|
||||
return uploadRes.fileID;
|
||||
},
|
||||
|
||||
onGoToDownloads() {
|
||||
@@ -47,7 +170,7 @@ Page({
|
||||
},
|
||||
|
||||
onGoToSettings() {
|
||||
wx.showToast({ title: '功能开发中', icon: 'none' });
|
||||
wx.navigateTo({ url: '/supportPages/settings/settings' });
|
||||
},
|
||||
|
||||
onGoToDebug() {
|
||||
|
||||
@@ -8,19 +8,14 @@
|
||||
<view class="profile-hero__tint"></view>
|
||||
<view class="profile-hero__avatar-wrap">
|
||||
<view class="profile-hero__avatar">
|
||||
<text
|
||||
class="profile-hero__avatar-emoji"
|
||||
wx:if="{{!avatarUrl}}"
|
||||
>🦆</text
|
||||
>
|
||||
<image
|
||||
wx:else
|
||||
class="profile-hero__avatar-img"
|
||||
src="{{avatarUrl}}"
|
||||
mode="aspectFill" />
|
||||
</view>
|
||||
<view
|
||||
class="profile-hero__edit-badge"
|
||||
bindtap="onOpenProfileEditor"
|
||||
aria-role="button"
|
||||
aria-label="编辑头像">
|
||||
<van-icon name="edit" size="28rpx" color="#6c5a00" />
|
||||
@@ -73,9 +68,13 @@
|
||||
</view>
|
||||
<text class="profile-row__label">打印指南</text>
|
||||
</view>
|
||||
<text class="profile-row__chevron">›</text>
|
||||
<toy-icon
|
||||
class="profile-row__chevron"
|
||||
name="arrow-right"
|
||||
size="40rpx"
|
||||
color="#9a958b" />
|
||||
</view>
|
||||
<view class="profile-row" bindtap="onGoToFeedback">
|
||||
<!-- <view class="profile-row" bindtap="onGoToFeedback">
|
||||
<view class="profile-row__left">
|
||||
<view class="profile-row__icon-bg">
|
||||
<van-icon
|
||||
@@ -85,8 +84,12 @@
|
||||
</view>
|
||||
<text class="profile-row__label">提交反馈</text>
|
||||
</view>
|
||||
<text class="profile-row__chevron">›</text>
|
||||
</view>
|
||||
<toy-icon
|
||||
class="profile-row__chevron"
|
||||
name="arrow-right"
|
||||
size="40rpx"
|
||||
color="#9a958b" />
|
||||
</view> -->
|
||||
<view class="profile-row" bindtap="onGoToSettings">
|
||||
<view class="profile-row__left">
|
||||
<view class="profile-row__icon-bg">
|
||||
@@ -97,7 +100,11 @@
|
||||
</view>
|
||||
<text class="profile-row__label">设置</text>
|
||||
</view>
|
||||
<text class="profile-row__chevron">›</text>
|
||||
<toy-icon
|
||||
class="profile-row__chevron"
|
||||
name="arrow-right"
|
||||
size="40rpx"
|
||||
color="#9a958b" />
|
||||
</view>
|
||||
<view
|
||||
wx:if="{{isDevEnv}}"
|
||||
@@ -109,9 +116,13 @@
|
||||
</view>
|
||||
<text class="profile-row__label">Debug 工具</text>
|
||||
</view>
|
||||
<text class="profile-row__chevron">›</text>
|
||||
<toy-icon
|
||||
class="profile-row__chevron"
|
||||
name="arrow-right"
|
||||
size="40rpx"
|
||||
color="#9a958b" />
|
||||
</view>
|
||||
<view
|
||||
<!-- <view
|
||||
class="profile-row profile-row--last"
|
||||
bindtap="onGoToAbout">
|
||||
<view class="profile-row__left">
|
||||
@@ -123,12 +134,16 @@
|
||||
</view>
|
||||
<text class="profile-row__label">关于涂鸦丫</text>
|
||||
</view>
|
||||
<text class="profile-row__chevron">›</text>
|
||||
</view>
|
||||
<toy-icon
|
||||
class="profile-row__chevron"
|
||||
name="arrow-right"
|
||||
size="40rpx"
|
||||
color="#9a958b" />
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<!-- 专业版推广(Stitch 深色卡) -->
|
||||
<view class="profile-upsell" bindtap="onGoToPremium">
|
||||
<!-- <view class="profile-upsell" bindtap="onGoToPremium">
|
||||
<van-icon
|
||||
name="gem-o"
|
||||
class="profile-upsell__deco"
|
||||
@@ -141,13 +156,73 @@
|
||||
>
|
||||
<view class="profile-upsell__btn">立即升级</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<view class="profile-version">
|
||||
<text class="profile-version__text"
|
||||
>Doodle Mini v{{version}}</text
|
||||
>
|
||||
<text class="profile-version__text">涂鸦丫 v{{version}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<van-popup
|
||||
show="{{showProfileEditor}}"
|
||||
position="bottom"
|
||||
round
|
||||
safe-area-inset-bottom="{{false}}"
|
||||
custom-class="profile-editor-popup"
|
||||
bind:close="onCloseProfileEditor">
|
||||
<view class="profile-editor">
|
||||
<view class="profile-editor__header">
|
||||
<text class="profile-editor__title">完善资料</text>
|
||||
<text class="profile-editor__desc"
|
||||
>选择头像并填写昵称后保存</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view class="profile-editor__avatar-section">
|
||||
<button
|
||||
class="profile-editor__avatar-button"
|
||||
open-type="chooseAvatar"
|
||||
bindchooseavatar="onChooseAvatar">
|
||||
<view class="profile-editor__avatar">
|
||||
<text
|
||||
class="profile-editor__avatar-placeholder"
|
||||
wx:if="{{!draftAvatarUrl}}"
|
||||
>选择头像</text
|
||||
>
|
||||
<image
|
||||
wx:else
|
||||
class="profile-editor__avatar-img"
|
||||
src="{{draftAvatarUrl}}"
|
||||
mode="aspectFill" />
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="profile-editor__field">
|
||||
<text class="profile-editor__label">昵称</text>
|
||||
<input
|
||||
class="profile-editor__input"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
maxlength="20"
|
||||
value="{{draftNickName}}"
|
||||
bindblur="onNicknameInput"
|
||||
bindinput="onNicknameInput" />
|
||||
</view>
|
||||
|
||||
<view class="profile-editor__actions">
|
||||
<view
|
||||
class="profile-editor__action profile-editor__action--ghost"
|
||||
bindtap="onCloseProfileEditor">
|
||||
取消
|
||||
</view>
|
||||
<view
|
||||
class="profile-editor__action profile-editor__action--primary {{isSavingProfile ? 'profile-editor__action--disabled' : ''}}"
|
||||
bindtap="onSubmitProfile">
|
||||
{{isSavingProfile ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</van-popup>
|
||||
</view>
|
||||
|
||||
@@ -200,45 +200,7 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async onBuildTap() {
|
||||
if (this.data.building) return;
|
||||
|
||||
const confirmed = await new Promise<boolean>((resolve) => {
|
||||
wx.showModal({
|
||||
title: '更新分类页数据',
|
||||
content:
|
||||
'将根据当前线上 worksheet 重新生成分类页数据,确认继续?',
|
||||
success: (res) => resolve(!!res.confirm),
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
this.setData({ building: true });
|
||||
|
||||
try {
|
||||
const result = await callCloudFunction<{
|
||||
version: number;
|
||||
stats: Record<string, number>;
|
||||
}>('pageContentBuild', { page: 'category' });
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || '生成失败');
|
||||
}
|
||||
|
||||
const version = result.data?.version || 0;
|
||||
wx.showToast({
|
||||
title: `生成成功 v${version}`,
|
||||
icon: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
wx.showToast({
|
||||
title:
|
||||
error instanceof Error ? error.message : '生成失败',
|
||||
icon: 'none',
|
||||
});
|
||||
} finally {
|
||||
this.setData({ building: false });
|
||||
}
|
||||
},
|
||||
// [v3.0] 分类页已改为实时接口查询,不再需要手动更新分类数据
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onBuildTap() {},
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Build Button -->
|
||||
<!-- [v3.0] 分类页已改为实时接口查询,不再需要手动更新分类数据
|
||||
<view class="ccm-page__build">
|
||||
<toy-button
|
||||
type="primary"
|
||||
@@ -101,4 +101,5 @@
|
||||
{{building ? '生成中...' : '更新分类页数据'}}
|
||||
</toy-button>
|
||||
</view>
|
||||
-->
|
||||
</view>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "打印指南",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#f7ee47",
|
||||
"backgroundColor": "#F6F6F6"
|
||||
"navigationBarBackgroundColor": "#F8F0E0",
|
||||
"backgroundColor": "#F8F0E0",
|
||||
"usingComponents": {
|
||||
"van-icon": "@vant/weapp/icon/index",
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,105 @@
|
||||
.guide-page {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
padding-bottom: 48rpx;
|
||||
@import '../../style/theme.less';
|
||||
|
||||
page {
|
||||
min-height: 100%;
|
||||
background: @bg-header;
|
||||
}
|
||||
|
||||
.guide-page {
|
||||
min-height: 100vh;
|
||||
background: @bg-header;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-page__body {
|
||||
padding: @section-gap-xs @page-padding-x;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Hero 主卡(参考 profile-hero) */
|
||||
.guide-page__hero {
|
||||
background: linear-gradient(180deg, #f7ee47 0%, #e6d520 100%);
|
||||
padding: 60rpx 40rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx @page-padding-inner-x 56rpx;
|
||||
background: @bg-white;
|
||||
border-radius: @radius-xl;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
box-shadow: @shadow;
|
||||
overflow: hidden;
|
||||
margin-bottom: @section-gap-xs;
|
||||
}
|
||||
|
||||
.guide-page__hero-tint {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 192rpx;
|
||||
background: @brand;
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.guide-page__hero-icon-wrap {
|
||||
position: relative;
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border-radius: 50%;
|
||||
background: @brand;
|
||||
border: 8rpx solid @bg-white;
|
||||
box-shadow: @shadow;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.guide-page__hero-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 72rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.guide-page__hero-title {
|
||||
margin-top: 32rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
font-weight: 800;
|
||||
color: @text-title;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.guide-page__hero-sub {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: @text-secondary;
|
||||
text-align: center;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* 步骤时间线卡片 */
|
||||
.guide-page__steps {
|
||||
position: relative;
|
||||
padding: 40rpx 32rpx 24rpx 32rpx;
|
||||
margin: 0 24rpx;
|
||||
margin-top: -24rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
padding: 56rpx 40rpx 16rpx;
|
||||
background: @bg-white;
|
||||
border-radius: @radius-xl;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
box-shadow: @shadow;
|
||||
margin-bottom: @section-gap-xs;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-page__timeline-line {
|
||||
position: absolute;
|
||||
left: 62rpx;
|
||||
top: 100rpx;
|
||||
bottom: 100rpx;
|
||||
left: 70rpx;
|
||||
top: 110rpx;
|
||||
bottom: 110rpx;
|
||||
width: 4rpx;
|
||||
background: #eee;
|
||||
background: @bg-gray;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@@ -48,62 +108,66 @@
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 48rpx;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
.guide-page__step--last {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.guide-page__step-num-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 64rpx;
|
||||
margin-right: 24rpx;
|
||||
margin-right: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.guide-page__step-num {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: #f7ee47;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
background: @brand;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: @text-selected-btn;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.guide-page__step-body {
|
||||
flex: 1;
|
||||
padding-top: 4rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.guide-page__step-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.guide-page__step-desc {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.55;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: @text-secondary;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 通用 Tip(弱提示) */
|
||||
.guide-page__tip {
|
||||
background: #fff8e1;
|
||||
border-radius: 16rpx;
|
||||
background: @bg-header;
|
||||
border-radius: @radius;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
font-size: 24rpx;
|
||||
color: @text-secondary;
|
||||
line-height: 1.55;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
}
|
||||
|
||||
.guide-page__tip-line {
|
||||
@@ -115,36 +179,114 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.guide-page__faq-title {
|
||||
font-size: 32rpx;
|
||||
/* 相册权限提示卡 */
|
||||
.guide-page__perm {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: @radius;
|
||||
background: fade(@brand, 18%);
|
||||
border: 1rpx solid fade(@brand, 36%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-page__perm--granted {
|
||||
background: fade(@brand, 12%);
|
||||
border-color: fade(@brand, 28%);
|
||||
}
|
||||
|
||||
.guide-page__perm--denied {
|
||||
background: fade(@brand, 22%);
|
||||
border-color: fade(@brand, 50%);
|
||||
}
|
||||
|
||||
.guide-page__perm-icon-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: @bg-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
.guide-page__perm-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.guide-page__perm-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
padding: 40rpx 32rpx 16rpx;
|
||||
color: @text-title;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.guide-page__perm-desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: @text-secondary;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.guide-page__perm-btn {
|
||||
align-self: flex-start;
|
||||
margin-top: 20rpx;
|
||||
padding: 14rpx 32rpx;
|
||||
border-radius: 999rpx;
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
box-shadow: @shadow;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* FAQ */
|
||||
.guide-page__faq-title {
|
||||
font-size: @fs-section-head-title;
|
||||
font-weight: 800;
|
||||
color: @text-title;
|
||||
padding: 16rpx 8rpx 24rpx;
|
||||
}
|
||||
|
||||
.guide-page__faq {
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.guide-page__faq-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
background: @bg-white;
|
||||
border-radius: @radius-lg;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
padding: 32rpx 36rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-page__faq-item--last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.guide-page__faq-q {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
color: @text-title;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.guide-page__faq-a {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
line-height: 1.55;
|
||||
}
|
||||
color: @text-secondary;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -1,7 +1,75 @@
|
||||
type AlbumAuthState = 'unknown' | 'granted' | 'denied';
|
||||
|
||||
const ALBUM_SCOPE = 'scope.writePhotosAlbum';
|
||||
|
||||
Page({
|
||||
data: {},
|
||||
data: {
|
||||
albumAuthState: 'unknown' as AlbumAuthState,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 静态说明页,无额外逻辑
|
||||
this.refreshAlbumAuthState();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.refreshAlbumAuthState();
|
||||
},
|
||||
|
||||
refreshAlbumAuthState() {
|
||||
wx.getSetting({
|
||||
success: (res) => {
|
||||
const auth = res.authSetting?.[ALBUM_SCOPE];
|
||||
let state: AlbumAuthState = 'unknown';
|
||||
if (auth === true) {
|
||||
state = 'granted';
|
||||
} else if (auth === false) {
|
||||
state = 'denied';
|
||||
}
|
||||
this.setData({ albumAuthState: state });
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({ albumAuthState: 'unknown' });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onHandleAlbumPerm() {
|
||||
const { albumAuthState } = this.data;
|
||||
|
||||
// 已被拒绝过:只能去系统设置打开
|
||||
if (albumAuthState === 'denied') {
|
||||
wx.openSetting({
|
||||
success: (res) => {
|
||||
const granted = !!res.authSetting?.[ALBUM_SCOPE];
|
||||
this.setData({
|
||||
albumAuthState: granted ? 'granted' : 'denied',
|
||||
});
|
||||
if (granted) {
|
||||
wx.showToast({
|
||||
title: '已开启相册权限',
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 未授权过:直接发起授权请求
|
||||
wx.authorize({
|
||||
scope: ALBUM_SCOPE,
|
||||
success: () => {
|
||||
this.setData({ albumAuthState: 'granted' });
|
||||
wx.showToast({
|
||||
title: '已开启相册权限',
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({ albumAuthState: 'denied' });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,63 +1,132 @@
|
||||
<view class="guide-page">
|
||||
<view class="guide-page__hero">
|
||||
<text class="guide-page__hero-icon">🖨️</text>
|
||||
<text class="guide-page__hero-title">3 步完成打印</text>
|
||||
</view>
|
||||
<nav-bar title="打印指南" />
|
||||
|
||||
<view class="guide-page__steps">
|
||||
<view class="guide-page__timeline-line" />
|
||||
|
||||
<view class="guide-page__step">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">1</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">保存到相册</text>
|
||||
<text class="guide-page__step-desc">在练习纸预览页点击「保存」或「保存到相册」,授权后即可保存图片。</text>
|
||||
<view class="guide-page__tip">提示:建议先关闭深色模式截图,打印对比度更好。</view>
|
||||
<view class="guide-page__body">
|
||||
<!-- Hero 卡片:白底大圆角 + 顶条品牌色(参考 profile 主卡) -->
|
||||
<view class="guide-page__hero">
|
||||
<view class="guide-page__hero-tint"></view>
|
||||
<view class="guide-page__hero-icon-wrap">
|
||||
<text class="guide-page__hero-icon">🖨️</text>
|
||||
</view>
|
||||
<text class="guide-page__hero-title">3 步完成打印</text>
|
||||
<text class="guide-page__hero-sub"
|
||||
>保存图片 → 连接打印机 → 一键打印</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view class="guide-page__step">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">2</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">连接打印机</text>
|
||||
<text class="guide-page__step-desc">确保打印机与手机处于同一 Wi-Fi,或按打印机说明完成蓝牙配对。</text>
|
||||
<view class="guide-page__tip">家用喷墨 / 激光打印机均可;A4 纸竖向打印即可。</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 步骤时间线 -->
|
||||
<view class="guide-page__steps">
|
||||
<view class="guide-page__timeline-line" />
|
||||
|
||||
<view class="guide-page__step guide-page__step--last">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">3</view>
|
||||
<view class="guide-page__step">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">1</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">保存到相册</text>
|
||||
<text class="guide-page__step-desc">
|
||||
在练习纸预览页点击「保存到相册」,授权后即可下载图片到手机相册。
|
||||
</text>
|
||||
|
||||
<!-- 相册权限提示(动态状态) -->
|
||||
<view
|
||||
class="guide-page__perm guide-page__perm--{{albumAuthState}}">
|
||||
<view class="guide-page__perm-icon-wrap">
|
||||
<van-icon
|
||||
wx:if="{{albumAuthState === 'granted'}}"
|
||||
name="checked"
|
||||
size="36rpx"
|
||||
color="#1fe000" />
|
||||
<van-icon
|
||||
wx:elif="{{albumAuthState === 'denied'}}"
|
||||
name="warning-o"
|
||||
size="36rpx"
|
||||
color="#e8793a" />
|
||||
<van-icon
|
||||
wx:else
|
||||
name="info-o"
|
||||
size="36rpx"
|
||||
color="#605b50" />
|
||||
</view>
|
||||
<view class="guide-page__perm-body">
|
||||
<text class="guide-page__perm-title"
|
||||
>{{albumAuthState === 'granted' ?
|
||||
'已开启相册权限' : albumAuthState === 'denied' ?
|
||||
'相册权限未开启' : '检查相册权限'}}</text
|
||||
>
|
||||
<text class="guide-page__perm-desc"
|
||||
>{{albumAuthState === 'granted' ?
|
||||
'可正常将练习纸保存到手机相册。' :
|
||||
albumAuthState === 'denied' ?
|
||||
'需要在系统设置中开启「保存到相册」权限,才能下载图片。'
|
||||
:
|
||||
'请确认是否已授权小程序保存图片到相册。'}}</text
|
||||
>
|
||||
<view
|
||||
wx:if="{{albumAuthState !== 'granted'}}"
|
||||
class="guide-page__perm-btn"
|
||||
bindtap="onHandleAlbumPerm"
|
||||
>{{albumAuthState === 'denied' ? '前往设置开启'
|
||||
: '立即检查授权'}}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">从相册打印</text>
|
||||
<text class="guide-page__step-desc">打开手机相册,选中刚保存的练习纸图片,点击分享或打印。</text>
|
||||
<view class="guide-page__tip">
|
||||
<text class="guide-page__tip-line">iPhone:分享 → 打印 → 选择打印机。</text>
|
||||
<text class="guide-page__tip-line">Android:更多 → 打印 或 使用系统相册自带的打印入口。</text>
|
||||
|
||||
<view class="guide-page__step">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">2</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">连接打印机</text>
|
||||
<text class="guide-page__step-desc">
|
||||
确保打印机与手机处于同一
|
||||
Wi-Fi,或按打印机说明完成蓝牙配对。
|
||||
</text>
|
||||
<view class="guide-page__tip">
|
||||
家用喷墨 / 激光打印机均可,A4 纸竖向打印即可。
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-page__step guide-page__step--last">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">3</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">从相册打印</text>
|
||||
<text class="guide-page__step-desc">
|
||||
打开手机相册,选中刚保存的练习纸图片,点击分享或打印。
|
||||
</text>
|
||||
<view class="guide-page__tip">
|
||||
<text class="guide-page__tip-line">
|
||||
iPhone:分享 → 打印 → 选择打印机。
|
||||
</text>
|
||||
<text class="guide-page__tip-line">
|
||||
Android:更多 → 打印,或使用系统相册自带的打印入口。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-page__faq-title">常见问题</view>
|
||||
<!-- FAQ -->
|
||||
<view class="guide-page__faq-title">常见问题</view>
|
||||
|
||||
<view class="guide-page__faq">
|
||||
<view class="guide-page__faq-item">
|
||||
<text class="guide-page__faq-q">保存失败怎么办?</text>
|
||||
<text class="guide-page__faq-a">请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。</text>
|
||||
</view>
|
||||
<view class="guide-page__faq-item">
|
||||
<text class="guide-page__faq-q">打印出来太小/太大?</text>
|
||||
<text class="guide-page__faq-a">在打印预览中选择「适应页面」或调整缩放比例,通常 100% 即可。</text>
|
||||
</view>
|
||||
<view class="guide-page__faq-item">
|
||||
<text class="guide-page__faq-q">没有打印机?</text>
|
||||
<text class="guide-page__faq-a">可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。</text>
|
||||
<view class="guide-page__faq">
|
||||
<view class="guide-page__faq-item">
|
||||
<text class="guide-page__faq-q">保存失败怎么办?</text>
|
||||
<text class="guide-page__faq-a">
|
||||
请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。
|
||||
</text>
|
||||
</view>
|
||||
<view class="guide-page__faq-item guide-page__faq-item--last">
|
||||
<text class="guide-page__faq-q">没有打印机?</text>
|
||||
<text class="guide-page__faq-a">
|
||||
可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "设置",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#F8F0E0",
|
||||
"backgroundColor": "#F8F0E0",
|
||||
"usingComponents": {
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@import '../../style/theme.less';
|
||||
|
||||
page {
|
||||
min-height: 100%;
|
||||
background: @bg-header;
|
||||
}
|
||||
|
||||
.settings-page {
|
||||
min-height: 100vh;
|
||||
background: @bg-header;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settings-body {
|
||||
padding: @section-gap-xs @page-padding-x;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background: @bg-white;
|
||||
border-radius: @radius-lg;
|
||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||
box-shadow: @shadow;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 36rpx @page-padding-inner-x;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: @page-padding-inner-x;
|
||||
right: @page-padding-inner-x;
|
||||
bottom: 0;
|
||||
height: 1rpx;
|
||||
background: rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
&:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(50, 46, 37, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-action__text {
|
||||
font-size: 30rpx;
|
||||
color: @text-primary;
|
||||
}
|
||||
|
||||
.settings-action--danger .settings-action__text {
|
||||
color: #e53e3e;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { clearUser } from '../../utils/auth';
|
||||
|
||||
Page({
|
||||
onClearProfile() {
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '删除后头像和昵称将恢复为默认状态,是否继续?',
|
||||
confirmColor: '#e53e3e',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.doClearProfile();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async doClearProfile() {
|
||||
wx.showLoading({ title: '处理中...', mask: true });
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'userSettings',
|
||||
data: { action: 'clearProfile' },
|
||||
});
|
||||
const result = (res?.result as any) || {};
|
||||
if (result.code !== 0) {
|
||||
throw new Error(result.message || '操作失败');
|
||||
}
|
||||
clearUser();
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
console.error('clearProfile failed', e);
|
||||
wx.showToast({ title: '操作失败,请稍后重试', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
onDeleteAccount() {
|
||||
wx.showModal({
|
||||
title: '注销账户',
|
||||
content:
|
||||
'注销后您的所有数据(包括收藏、下载记录等)将被永久删除且无法恢复,确认要注销吗?',
|
||||
confirmText: '确认注销',
|
||||
confirmColor: '#e53e3e',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.doDeleteAccount();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async doDeleteAccount() {
|
||||
wx.showLoading({ title: '处理中...', mask: true });
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'userSettings',
|
||||
data: { action: 'deleteAccount' },
|
||||
});
|
||||
const result = (res?.result as any) || {};
|
||||
if (result.code !== 0) {
|
||||
throw new Error(result.message || '操作失败');
|
||||
}
|
||||
clearUser();
|
||||
wx.hideLoading();
|
||||
|
||||
wx.reLaunch({ url: '/pages/home/home' });
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
console.error('deleteAccount failed', e);
|
||||
wx.showToast({ title: '操作失败,请稍后重试', icon: 'none' });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<view class="settings-page">
|
||||
<nav-bar title="设置" />
|
||||
|
||||
<view class="settings-body">
|
||||
<view class="settings-section">
|
||||
<view class="settings-action" bindtap="onClearProfile">
|
||||
<text class="settings-action__text">删除头像和昵称</text>
|
||||
</view>
|
||||
<view
|
||||
class="settings-action settings-action--danger"
|
||||
bindtap="onDeleteAccount">
|
||||
<text class="settings-action__text">注销账户</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -19,18 +19,40 @@ let currentUser: UserInfo | null = null;
|
||||
/** 登录进行中的 Promise(防止并发重复调用) */
|
||||
let loginPromise: Promise<UserInfo> | null = null;
|
||||
|
||||
function syncAppGlobalUser(user: UserInfo | null): void {
|
||||
try {
|
||||
const app = getApp<IAppOption>();
|
||||
if (app?.globalData) {
|
||||
app.globalData.user = user;
|
||||
}
|
||||
} catch {
|
||||
// App 尚未初始化时忽略
|
||||
}
|
||||
}
|
||||
|
||||
function syncUserState(user: UserInfo, persist = true): UserInfo {
|
||||
currentUser = user;
|
||||
if (persist) {
|
||||
saveToStorage(user);
|
||||
}
|
||||
syncAppGlobalUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户,优先内存 → Storage → 云函数登录
|
||||
*/
|
||||
export async function getUser(): Promise<UserInfo> {
|
||||
// 1. 内存中有,直接返回
|
||||
if (currentUser) return currentUser;
|
||||
if (currentUser) {
|
||||
syncAppGlobalUser(currentUser);
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
// 2. 尝试从 Storage 读取
|
||||
const cached = loadFromStorage();
|
||||
if (cached) {
|
||||
currentUser = cached;
|
||||
return cached;
|
||||
return syncUserState(cached, false);
|
||||
}
|
||||
|
||||
// 3. 静默登录(防止并发)
|
||||
@@ -56,9 +78,18 @@ async function silentLogin(): Promise<UserInfo> {
|
||||
}
|
||||
|
||||
const user = result.data as UserInfo;
|
||||
currentUser = user;
|
||||
saveToStorage(user);
|
||||
return user;
|
||||
return syncUserState(user);
|
||||
}
|
||||
|
||||
export function getCachedUser(): UserInfo | null {
|
||||
if (currentUser) {
|
||||
syncAppGlobalUser(currentUser);
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
const cached = loadFromStorage();
|
||||
if (!cached) return null;
|
||||
return syncUserState(cached, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,6 +127,7 @@ function saveToStorage(user: UserInfo): void {
|
||||
*/
|
||||
export function clearUser(): void {
|
||||
currentUser = null;
|
||||
syncAppGlobalUser(null);
|
||||
wx.removeStorageSync(STORAGE_KEY);
|
||||
wx.removeStorageSync(STORAGE_EXPIRE_KEY);
|
||||
}
|
||||
@@ -108,4 +140,26 @@ export async function refreshUser(): Promise<UserInfo> {
|
||||
return await silentLogin();
|
||||
}
|
||||
|
||||
export async function updateUserProfile(payload: {
|
||||
nickName?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
}): Promise<UserInfo> {
|
||||
await getUser();
|
||||
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'userLogin',
|
||||
data: {
|
||||
action: 'updateProfile',
|
||||
...payload,
|
||||
},
|
||||
});
|
||||
const result = (res?.result as any) || {};
|
||||
|
||||
if (result.code !== 0 || !result.data) {
|
||||
throw new Error(result.message || '更新用户资料失败');
|
||||
}
|
||||
|
||||
return syncUserState(result.data as UserInfo);
|
||||
}
|
||||
|
||||
export type { UserInfo };
|
||||
|
||||
@@ -54,3 +54,21 @@ export async function getDownloadLogs(
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户下载(打印)总数
|
||||
*/
|
||||
export async function getDownloadLogCount(): Promise<number> {
|
||||
try {
|
||||
await getUser();
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'userDownloadLogs',
|
||||
data: { action: 'count' },
|
||||
});
|
||||
const result = (res?.result as any) || {};
|
||||
return result.code === 0 ? Number(result.data?.count || 0) : 0;
|
||||
} catch (e) {
|
||||
console.error('getDownloadLogCount failed', e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,24 @@ export async function getFavoriteList(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户收藏总数
|
||||
*/
|
||||
export async function getFavoriteCount(): Promise<number> {
|
||||
try {
|
||||
await getUser();
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'userFavorites',
|
||||
data: { action: 'count' },
|
||||
});
|
||||
const result = (res?.result as any) || {};
|
||||
return result.code === 0 ? Number(result.data?.count || 0) : 0;
|
||||
} catch (e) {
|
||||
console.error('getFavoriteCount failed', e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已收藏
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user