
简介这是一份面向计算机相关专业在校学生与教师的课程大作业级微信小程序源码聚焦打牌场景下的多人记账需求采用TypeScript开发兼顾工程规范性与教学实用性。资源共50个文件包含17个TypeScript核心逻辑文件如http.ts、util.ts、app.ts、11个JSON配置与数据文件project.config.json、sitemap.json等、11张UI图标PNG资源以及WXML页面结构、WXSS样式和README说明文档整体仅308KB轻量易读。已有113人学习下载适合作为前端入门实践、微信小程序课程设计或期末大作业参考亦可快速拓展为毕设原型——项目目录结构清晰模块划分明确pages/、components/、utils/、types/附带完整类型定义index.d.ts与ESLint配置便于理解TS工程化落地细节与小程序生命周期管理逻辑。1. 项目缘起与核心价值最近在整理过往的课程作业和练手项目时翻到了一个基于TypeScript开发的微信小程序源码主题是“打牌记账”。这虽然是一个课程作业但麻雀虽小五脏俱全从技术选型、架构设计到具体的业务逻辑实现都踩过不少坑也积累了一些在微信小程序开发中结合TypeScript的实战心得。很多同学在入门小程序开发时往往直接从JavaScript开始对类型安全和工程化缺乏概念导致项目稍大一点就难以维护。这个项目恰好可以作为一个不错的参考案例展示如何在一个具体的业务场景棋牌记账下运用TypeScript来构建一个结构清晰、可维护性高的小程序。这个“打牌记账”小程序的核心功能并不复杂主要就是记录牌局参与人、输赢分数并最终进行结算。但它涉及了小程序开发中几个非常典型的模块页面导航、表单输入、列表渲染、数据存储以及简单的状态管理。用TypeScript来重构或从头实现这类项目能让你深刻体会到静态类型检查在减少低级错误、提升开发体验和代码可读性方面的巨大优势。无论你是正在学习微信小程序开发还是想尝试将TypeScript引入现有项目这个源码剖析和经验分享或许都能给你带来一些启发。接下来我会从项目结构、TypeScript配置、核心功能实现以及开发中遇到的典型问题这几个方面带你一起拆解这个项目。2. 项目结构与技术栈选型解析拿到一个项目源码首先看它的目录结构和技术栈这能快速了解项目的工程化水平和开发者的思考路径。这个“打牌记账”小程序采用了比较标准的微信小程序项目结构并集成了TypeScript。2.1 目录结构剖析典型的项目根目录下会有以下关键部分miniprogram/ ├── app.ts // 小程序入口文件使用.ts后缀 ├── app.json ├── app.wxss ├── project.config.json // 项目配置文件需开启TypeScript编译 ├── tsconfig.json // TypeScript配置文件核心 ├── typings/ // 类型声明文件夹可选用于补充微信API类型 │ └── wx.d.ts ├── pages/ // 页面目录 │ ├── index/ // 首页创建/加入牌局 │ │ ├── index.ts │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.json │ ├── game/ // 游戏进行页记录分数 │ │ └── ... │ └── settlement/ // 结算页 │ └── ... ├── components/ // 自定义组件目录如果项目有抽取组件 │ └── player-item/ │ ├── player-item.ts │ ├── player-item.wxml │ └── ... ├── models/ // 数据模型/类型定义目录TypeScript优势体现区 │ ├── player.ts │ ├── game.ts │ └── record.ts ├── services/ // 服务层封装网络请求或本地存储操作 │ └── storage.ts └── utils/ // 工具函数 └── calculator.ts这个结构已经超出了基础小程序的范畴引入了models、services这样的分层概念这是TypeScript项目促进良好架构的体现。typings/wx.d.ts文件尤其重要它可能用于补充微信小程序API的类型定义确保在.ts文件中调用wx.request、wx.setStorage等方法时能有完整的类型提示和检查。2.2 TypeScript环境配置要点在微信小程序中使用TypeScript核心在于project.config.json和tsconfig.json这两个配置文件。首先在project.config.json中需要确保开启了开发者工具的TypeScript编译支持{ setting: { compileType: typescript, typescriptEnable: true, // ... 其他设置 }, // ... 其他配置 }这一步是告诉微信开发者工具“这个项目我要用TypeScript写请你用TS编译器来处理我的.ts文件。”其次tsconfig.json是TypeScript项目的灵魂。对于微信小程序一个基础的配置可能如下{ compilerOptions: { target: es2015, module: commonjs, lib: [es2015], allowJs: true, outDir: ./dist, rootDir: ./, strict: true, noImplicitAny: false, // 小程序中一些场景如data初始值可能暂时设为false strictNullChecks: true, moduleResolution: node, baseUrl: ./, paths: { /*: [miniprogram/*] // 配置路径别名方便导入 }, typeRoots: [./typings, ./node_modules/types], types: [miniprogram-api-typings] // 引入微信小程序官方类型定义包 }, include: [./miniprogram/**/*.ts], exclude: [node_modules] }这里有几个关键点target: 设置为es2015或es6因为微信小程序JavaScript运行环境支持ES6语法。module: 必须为commonjs因为小程序目前使用的是CommonJS模块系统。lib: 只需es2015避免引入DOM等浏览器特有库减少干扰。typeRoots和types: 这是获取微信API类型提示的关键。你需要通过npm安装微信小程序的官方类型定义包npm install miniprogram-api-typings --save-dev。然后在types中引入这样在代码中写wx.时编辑器就能自动补全所有API了。paths: 配置路径别名如/是个好习惯能避免冗长的相对路径如../../../models/player。注意关于网络热词中提到的“选项‘baseUrl’已弃用并将停止在 typescript 7.0 中运行。指定 compileroption”。这是一个重要的版本变更提示。在较新的TypeScript版本中baseUrl和paths的配置关系发生了变化。为了向前兼容建议同时明确配置baseUrl如上例设置为./并确保paths中的路径是基于baseUrl的。未来如果升级到TS 7.0应关注官方迁移指南可能的做法是更明确地使用rootDirs或其他配置。2.3 为何选择TypeScript而非纯JavaScript对于课程作业或小型项目这个问题值得思考。选择TS主要基于以下几点类型安全与错误前置在编写“打牌记账”逻辑时玩家分数是number类型名字是string类型。用JS写不小心把字符串和数字相加可能得到诡异结果如“玩家1” 10 “玩家110”运行时才报错。TS在写代码时就会标红提示强迫你处理类型问题将很多运行时错误消灭在编码阶段。代码可读性与维护性models/目录下的.ts文件定义了清晰的数据接口。例如Player接口明确说明了一个玩家对象应该有哪些字段。半年后回头看代码或者别人接手项目一眼就能看懂数据结构无需猜测。更好的开发体验IDE支持配合miniprogram-api-typings在VSCode等编辑器中输入wx.setStorage会自动弹出参数提示告诉你第一个参数是key: string第二个参数是data: any大大减少查文档的频率。便于重构如果后期想修改“结算规则”比如从“积分制”改为“金币制”在TS中你修改了Game接口的类型定义所有用到的地方都会报错引导你一一修正避免遗漏。当然代价是初期需要学习类型语法和配置环境。但对于旨在学习和体现工程能力的课程作业来说这个投入是非常值得的。3. 核心业务逻辑与TypeScript实现细节接下来我们深入到“打牌记账”这个具体业务中看看TypeScript如何帮助实现清晰、健壮的代码。3.1 数据模型定义Models这是TypeScript大显身手的地方。在models/目录下我们定义核心数据的形状。models/player.ts:// 定义玩家接口 export interface IPlayer { id: string; // 唯一标识可以用生成函数创建 name: string; avatarUrl?: string; // 头像URL可选字段 initialScore: number; // 初始分数例如每人100分 currentScore: number; // 当前分数随牌局变化 } // 创建一个玩家的工厂函数确保初始化数据完整 export function createPlayer(name: string, initialScore: number 100): IPlayer { return { id: player_${Date.now()}_${Math.random().toString(36).substr(2, 9)}, name, initialScore, currentScore: initialScore, // 开始时当前分数等于初始分数 }; }这里定义了IPlayer接口并使用?定义了可选字段avatarUrl。同时提供了一个工厂函数createPlayer用于规范地创建玩家对象避免散落在各处的{id: ..., name: ...}写法不一致。models/game.ts:import { IPlayer } from ./player; // 定义单条分数记录 export interface IScoreRecord { playerId: string; // 关联玩家ID round: number; // 第几轮 scoreChange: number; // 分数变化正为赢负为输 timestamp: number; // 记录时间戳 } // 定义牌局游戏 export interface IGame { id: string; name: string; // 牌局名称如“周末麻将局” players: IPlayer[]; scoreRecords: IScoreRecord[]; createdAt: number; status: active | settled; // 使用字面量类型限定状态值 currentRound: number; } // 初始化一个游戏 export function initGame(name: string, players: IPlayer[]): IGame { if (players.length 2) { throw new Error(至少需要2名玩家才能开始游戏); } return { id: game_${Date.now()}, name, players, scoreRecords: [], createdAt: Date.now(), status: active, currentRound: 1, }; }这里的关键是使用了字面量类型(active | settled) 来定义status字段。这意味着game.status只能赋值为这两个字符串之一如果你不小心写成了finishedTS编译器会立刻报错。这比用字符串常量更安全、更直观。3.2 页面组件中的类型化实践有了清晰的数据模型页面和组件的逻辑就变得简单明了。以记录分数的页面 (pages/game/index.ts) 为例// pages/game/index.ts import { IGame, IScoreRecord } from ../../models/game; import { IPlayer } from ../../models/player; PageDataOption, CustomOption({ data: { game: null as IGame | null, // 初始为null加载后赋值 currentPlayerId: , scoreInput: , // 绑定输入框的值是字符串 }, onLoad(options: { gameId: string }) { // options 被类型化为 { gameId: string } const gameId options.gameId; this.loadGameData(gameId); }, loadGameData(gameId: string) { // 从本地存储或全局状态获取游戏数据 const game getApp().globalData.games.find((g: IGame) g.id gameId); if (game) { this.setData({ game }); if (game.players.length 0) { this.setData({ currentPlayerId: game.players[0].id }); } } else { wx.showToast({ title: 牌局不存在, icon: error }); setTimeout(() wx.navigateBack(), 1500); } }, onPlayerChange(e: WechatMiniprogram.PickerChange) { // e.detail.value 类型来自微信API类型定义 const index parseInt(e.detail.value as string, 10); const player this.data.game!.players[index]; // 使用非空断言因为此时game已加载 this.setData({ currentPlayerId: player.id }); }, onScoreInput(e: WechatMiniprogram.Input) { // e.detail.value 类型为 string const value e.detail.value; // 简单的输入校验只允许输入数字包括负数 if (/^-?\d*$/.test(value)) { this.setData({ scoreInput: value }); } }, addRecord() { const { game, currentPlayerId, scoreInput } this.data; if (!game || !currentPlayerId || scoreInput ) { wx.showToast({ title: 请填写完整, icon: none }); return; } const scoreChange parseInt(scoreInput, 10); if (isNaN(scoreChange)) { return; } const newRecord: IScoreRecord { playerId: currentPlayerId, round: game.currentRound, scoreChange, timestamp: Date.now(), }; // 更新游戏数据 const updatedGame: IGame { ...game, scoreRecords: [...game.scoreRecords, newRecord], // 更新对应玩家的当前分数 players: game.players.map(p p.id currentPlayerId ? { ...p, currentScore: p.currentScore scoreChange } : p ), }; // 保存到全局状态或存储 getApp().globalData.games getApp().globalData.games.map((g: IGame) g.id updatedGame.id ? updatedGame : g ); this.setData({ game: updatedGame, scoreInput: , // 清空输入框 }); wx.showToast({ title: 记录成功 }); }, });在这个页面中data中的game被明确类型化为IGame | null清晰地表达了其可能的状态。事件处理函数e的参数使用了微信小程序API的类型如WechatMiniprogram.PickerChange和WechatMiniprogram.Input这来自于我们安装的miniprogram-api-typings包提供了完美的代码提示。创建newRecord时必须符合IScoreRecord接口字段名或类型错误都会立即被提示。更新数据时我们使用了展开运算符 (...) 来创建新的对象避免直接修改原数据这是配合React/小程序这类数据驱动视图框架的良好实践TS能确保我们新对象的类型正确。3.3 工具函数与计算逻辑结算功能涉及计算这部分逻辑适合放在utils/目录下并用TypeScript保证计算过程的类型安全。utils/calculator.ts:import { IGame, IPlayer } from ../models; // 计算每个玩家的净输赢 export interface ISettlementResult { playerId: string; playerName: string; netChange: number; // 净输赢正数为赢负数为输 } export function calculateSettlement(game: IGame): ISettlementResult[] { // 初始化一个Map来累加每个玩家的总分变化 const scoreMap new Mapstring, number(); // 遍历所有记录累加变化 game.scoreRecords.forEach(record { const current scoreMap.get(record.playerId) || 0; scoreMap.set(record.playerId, current record.scoreChange); }); // 转换为结果数组并关联玩家信息 const results: ISettlementResult[] []; scoreMap.forEach((netChange, playerId) { const player game.players.find(p p.id playerId); if (player) { results.push({ playerId, playerName: player.name, netChange, }); } }); // 按净输赢从高到低排序 return results.sort((a, b) b.netChange - a.netChange); } // 校验分数记录是否平衡例如麻将中所有玩家分数变化之和应为0 export function isScoreBalanced(game: IGame): boolean { const totalChange game.scoreRecords.reduce((sum, record) sum record.scoreChange, 0); // 考虑到浮点数计算允许一个极小的误差 return Math.abs(totalChange) 0.01; }这个工具模块输出了明确的接口ISettlementResult和纯函数calculateSettlement。函数接收一个IGame类型的参数并返回ISettlementResult[]。这种强类型约束使得函数的使用者和维护者都非常清楚数据的流入流出格式极大降低了沟通和理解成本。同时像isScoreBalanced这样的校验函数其意图通过函数名和类型就一目了然。4. 本地数据持久化方案与类型安全存取微信小程序提供了wx.setStorage和wx.getStorage等API进行本地数据存储。在JavaScript中我们存取的数据是“黑盒”取出来后再进行类型断言或转换。而在TypeScript中我们可以封装一个类型安全的存储服务。services/storage.ts:// 定义存储的键名枚举避免硬编码 export enum StorageKey { GAME_LIST game_list_v1, // 加版本后缀便于未来数据结构升级时迁移 USER_SETTINGS user_settings, } // 一个通用的、类型安全的存储函数 export function setTypedStorageT(key: StorageKey, data: T): Promisevoid { return new Promise((resolve, reject) { wx.setStorage({ key, data, success: () resolve(), fail: (err) reject(err), }); }); } // 一个通用的、类型安全的读取函数 export function getTypedStorageT(key: StorageKey): PromiseT | null { return new Promise((resolve, reject) { wx.getStorage({ key, success: (res) resolve(res.data as T), fail: (err) { // 如果没找到对应的key不算错误返回null if (err.errMsg.includes(data not found)) { resolve(null); } else { reject(err); } }, }); }); } // 针对游戏列表的专用方法 export async function saveGameList(games: IGame[]): Promisevoid { await setTypedStorageIGame[](StorageKey.GAME_LIST, games); } export async function loadGameList(): PromiseIGame[] { const games await getTypedStorageIGame[](StorageKey.GAME_LIST); return games || []; // 如果没数据返回空数组 }这个服务层做了几件重要的事使用枚举定义Key避免了在代码中散落字符串game_list集中管理修改方便。泛型函数setTypedStorageT和getTypedStorageT是泛型函数。调用saveGameList时它内部调用setTypedStorageIGame[]这意味着存入的数据被期望是IGame[]类型。读取时getTypedStorageIGame[]会返回PromiseIGame[] | null。这就在编译阶段建立了类型契约。异步Promise封装将微信回调风格的API封装成更现代的Promise形式方便使用async/await语法。错误处理在getTypedStorage中对“数据不存在”这种常见情况做了特殊处理返回null而不是抛出错误使业务逻辑更简洁。在页面中使用时// 在app.ts或某个页面中 import { loadGameList, saveGameList } from ./services/storage; import { IGame } from ./models/game; async function onLoad() { try { const games: IGame[] await loadGameList(); // 这里games的类型明确为IGame[] getApp().globalData.games games; } catch (error) { console.error(加载游戏列表失败:, error); wx.showToast({ title: 加载数据失败, icon: error }); } }这样一来从存储中取出的games变量TypeScript就知道它是一个IGame[]数组你可以安全地调用.map,.find等方法并且访问game.players属性时会有完整的代码提示。5. 开发与构建过程中的常见问题与解决方案在实际开发这个TypeScript小程序作业的过程中我遇到了几个颇具代表性的问题这里分享出来希望能帮你绕过这些坑。5.1 类型定义缺失或冲突问题问题描述在安装miniprogram-api-typings后大部分API都有类型提示了但有时一些较新的小程序API或者特定组件的方法可能找不到定义或者自定义组件的properties和methods在父页面中调用时没有类型提示。解决方案补充类型声明在项目根目录的typings文件夹下或直接在miniprogram目录下创建自定义的.d.ts文件。例如为自定义组件补充类型// typings/custom-components.d.ts declare namespace WechatMiniprogram.Component { interface InstanceProperties { // 假设你有一个自定义的玩家选择器组件它有一个selectedId属性 selectedId?: string; } interface InstanceMethods { // 以及一个resetSelection方法 resetSelection: () void; } }注意这种全局扩充需要谨慎最好有文档记录。使用类型断言在明确知道数据形状的情况下可以使用as进行类型断言。例如从wx.getStorage取出的数据如果你确信它的结构const rawData wx.getStorageSync(my_data); const myData rawData as IMyDataType; // 强制断言这是一种“告诉TypeScript编译器请相信我”的方式要确保你的断言是正确的否则会导致运行时错误。更新类型包定期检查并更新miniprogram-api-typings到最新版本以获取最新的API类型定义。5.2 小程序Page/Component构造器的类型标注问题描述在编写Page或Component构造函数时如何为data、methods以及生命周期函数提供准确的类型标准做法 微信小程序官方类型定义提供了泛型参数。对于Page通常是PageData, Custom。Data指定data对象的类型。Custom指定自定义方法在methods中定义的类型。// 定义页面的数据类型 interface PageData { count: number; list: string[]; game: IGame | null; } // 定义页面的自定义方法类型 interface PageCustom { onMyCustomTap: () void; fetchData: (id: string) Promisevoid; } PagePageData, PageCustom({ data: { count: 0, list: [], game: null, // 这里必须符合PageData类型 }, onMyCustomTap() { // 这个方法可以在wxml中绑定 this.setData({ count: this.data.count 1 }); }, async fetchData(id: string) { // 异步方法 const data await someAsyncFunction(id); this.setData({ list: data }); }, onLoad() { // 生命周期函数 this.fetchData(123); // 调用自定义方法有类型提示 }, });对于Component也是类似的使用ComponentData, Props, Custom泛型。明确地为每个页面或组件定义接口虽然初期有些繁琐但带来的类型安全和代码提示收益是巨大的。5.3 真机调试与类型检查的边界问题描述在微信开发者工具中TypeScript编译检查通过但在真机上运行时可能出现undefined或null的错误尤其是在网络请求回调或异步操作中。经验与策略启用严格空值检查在tsconfig.json中设置strictNullChecks: true。这会强制你处理所有可能为null或undefined的情况。上面代码中data里的game: IGame | null就是例子。在访问this.data.game.players之前你必须先判断this.data.game是否存在。// 错误Object is possibly null. const players this.data.game.players; // 正确使用可选链?.和空值合并?? const players this.data.game?.players ?? []; // 或者使用条件判断 if (this.data.game) { const players this.data.game.players; // ... 安全操作 }异步数据的状态管理对于从网络或存储加载的数据在data中初始化为null或空状态是一种好习惯。在WXML模板中也要使用条件渲染来避免访问不存在的属性。!-- wxml模板 -- view wx:if{{game}} text牌局名称{{game.name}}/text view wx:for{{game.players}} wx:keyid {{item.name}} - 分数{{item.currentScore}} /view /view view wx:else加载中.../view善用非空断言谨慎在极少数你百分百确定某个时刻值一定不为空的情况下可以使用非空断言操作符!。// 在onLoad中已经成功加载了game后续某个同步函数中 const gameName this.data.game!.name; // 使用!断言非空但要慎用滥用会丧失类型检查的意义。通常更好的做法是优化代码逻辑确保在访问前值已就位。5.4 分包与异步化下的类型挑战问题描述当项目体积增大使用分包或分包异步化时类型引用可能会变得复杂。主包中的类型定义如何被分包中的组件使用解决方案将通用类型定义放在主包将models/、utils/和通用的services/放在主包目录下例如miniprogram根目录。这样所有分包都能引用到。使用路径别名在tsconfig.json中配置paths如/*: [miniprogram/*]。这样在分包中你可以通过import { IGame } from /models/game;来引入类型而不需要计算复杂的相对路径如../../../models/game。注意编译作用域确保tsconfig.json中的include字段包含了所有需要编译的.ts文件包括分包中的文件。例如include: [./miniprogram/**/*.ts, ./packageA/**/*.ts]。关于网络热词中提到的“微信小程序 分包异步化 在其它分包中的插”这指的是分包异步化特性它允许独立分包在运行时异步加载。对于TypeScript项目关键是要确保异步加载的组件或页面所依赖的类型定义通常来自主包或npm包在编译时是可用的。只要类型定义文件的物理路径在编译范围内通过include配置并且引用路径正确使用配置好的别名TypeScript就能正常进行类型检查运行时则是另一套模块加载机制。6. 从课程作业到可发布项目的优化思路这个“打牌记账”作为课程作业实现了核心功能。但如果想把它打磨成一个更完善、可发布的小程序还可以从以下几个方面进行优化这些优化点同样适用于其他TypeScript小程序项目。6.1 状态管理的升级目前示例中使用了getApp().globalData来共享游戏数据。这在简单场景下可行但随着功能复杂比如多个页面同时修改数据、需要响应式更新视图会变得难以维护。可以考虑引入更专业的状态管理库例如MobX对于小程序有很好的适配如mobx-miniprogram和mobx-miniprogram-bindings其响应式理念与小程序setData结合能实现自动更新视图代码更简洁。或者采用更轻量的发布-订阅模式自己封装一个状态管理中心。使用MobX后你的Game模型可以变成一个可观察对象任何对它的修改都会自动触发依赖其数据的组件更新无需手动调用setData。6.2 用户体验与性能优化数据缓存与同步当前数据只存在内存和本地存储。可以考虑加入云开发数据库实现多端数据同步前提是项目接入了云开发。TypeScript同样可以为云数据库的集合定义接口保证读写操作的类型安全。列表性能结算页或历史记录页如果列表很长需要使用wx:for的优化技巧如指定唯一的wx:key或对于超长列表考虑使用虚拟列表组件。输入体验在记录分数时除了文本输入可以考虑增加快捷按钮如“10”、“-5”提升操作效率。这涉及到WXML中绑定多个事件和TS中处理更复杂的逻辑。6.3 代码组织与重构建议自定义组件抽取如果“玩家信息展示”、“分数输入面板”、“结算结果行”等在多个页面重复出现应将其抽离为自定义组件。用TypeScript编写组件时需要明确定义properties和methods的类型。ComponentPlayerItemData, PlayerItemProps, PlayerItemCustom({ properties: { player: { type: Object as PropTypeIPlayer, // 使用PropType工具类型 value: null, }, highlight: { type: Boolean, value: false, }, }, methods: { onTap() { this.triggerEvent(playertap, { playerId: this.properties.player.id }); }, }, });hooks风格的逻辑复用对于“加载数据”、“表单验证”等通用逻辑可以封装成自定义的Composition Functions类似于Vue 3的Composition API或React Hooks的思想在多个页面或组件中复用。虽然小程序没有原生hooks但可以封装成纯函数来复用逻辑。6.4 构建与部署流程对于课程作业可能直接在微信开发者工具中点击“上传”即可。但对于团队协作或持续集成可以考虑代码风格与质量集成ESLint和Prettier并配置适用于TypeScript和小程序的规则在提交代码前自动格式化。环境变量区分开发、测试、生产环境例如云环境ID、API地址等。可以通过编写不同的配置文件在构建时注入。CI/CD使用Jenkins、GitHub Actions等工具在代码推送到仓库后自动执行类型检查、代码风格检查、打包并上传到小程序后台作为体验版。回过头看这个基于TypeScript的“打牌记账”微信小程序课程作业其价值远不止于实现记账功能本身。它更像一个微型样板工程展示了如何在微信小程序这个特定生态中系统地应用现代前端开发中类型安全、清晰架构和工程化的思想。从定义严谨的数据模型到封装类型安全的存储服务再到处理页面组件中的各种边界情况每一步都因为TypeScript的介入而变得更加可控和可预测。本文还有配套的精品资源点击获取