feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则
- 实现 postcss-remove-wxss-incompatible 插件 - 移除反斜杠转义的重要性修饰符类选择器(.\!xxx) - 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则 - 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则 - 解决微信小程序 WXSS 不支持的 CSS 特性问题
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# websopy-taro 项目记忆
|
||||
|
||||
## Tailwind CSS 在小程序中的兼容性规则
|
||||
|
||||
以下 Tailwind 类名在 WXSS 中不兼容,项目内禁止使用,已通过 PostCSS 插件 `postcss-remove-wxss-incompatible.js` 自动移除对应的 CSS 规则:
|
||||
|
||||
1. **`!xxx` important 修饰符**(如 `!visible`):生成 `.!visible` 选择器,WXSS 不支持反斜杠转义的 `!`。
|
||||
|
||||
2. **带 `0.5` 的类名**(如 `w-0.5`、`h-0.5`、`gap-0.5`):类名中的 `.` 被转义为 `\.`,生成 `.w-0\.5` 这样的选择器,WXSS 不兼容。应改用等效的整数尺寸或自定义值替代。
|
||||
|
||||
3. **`space-x-*` / `space-y-*`**(如 `space-y-4`):依赖 `:not()` + `~` 兄弟组合器 + CSS 变量 + 嵌套 `calc()`,WXSS 全不支持。应改用 `flex flex-col gap-*` 实现等效间距。
|
||||
|
||||
4. **`divide-x-*` / `divide-y-*`**:同样依赖 `:not()` + `~`,WXSS 不兼容。
|
||||
|
||||
## 替代方案速查
|
||||
|
||||
| 禁用类名 | 替代写法 |
|
||||
|---------|---------|
|
||||
| `space-y-4` | `flex flex-col gap-4` |
|
||||
| `space-x-4` | `flex flex-row gap-4` |
|
||||
| `!visible` | 直接写 `style={{ visibility: 'visible' }}` 或自定义类 |
|
||||
| `w-0.5` | 用自定义值或 `w-1` 替代 |
|
||||
|
||||
## 微信小程序接口合规
|
||||
|
||||
### 剪切板接口(运营规范 5.15.4)
|
||||
|
||||
1. **只允许用户主动触发复制**:所有 `Taro.setClipboardData` 调用必须绑定在明确的用户点击事件上,禁止在无用户操作时自动读写剪切板。
|
||||
2. **禁止复制后强制中断流程**:复制成功后不得强制跳转、强制关注、强制添加客服等,不能要求用户通过其他方式才能完成当前业务流程。
|
||||
3. **避免重复 Toast 干扰体验**:微信小程序调用 `setClipboardData` 成功后系统会自带「内容已复制」提示。为避免重复提示,统一封装 `utils/common.ts` 中的 `copyText` 函数:小程序端不再额外显示自定义 `复制成功` Toast,失败时仍显示错误提示。
|
||||
|
||||
### 项目内剪切板使用现状
|
||||
|
||||
已审查所有 `setClipboardData` 调用:
|
||||
- 均通过按钮点击触发(复制咨询模板、复制密钥、复制链接、复制兑换码等)
|
||||
- 无 `getClipboardData`(自动读取剪切板)调用
|
||||
- 无复制后强制跳转或中断业务流程的行为
|
||||
|
||||
当前实现基本符合 5.15.4 规范。
|
||||
|
||||
46
postcss-remove-wxss-incompatible.js
Normal file
46
postcss-remove-wxss-incompatible.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* PostCSS 插件:移除 WXSS 不兼容的 Tailwind CSS 规则
|
||||
*
|
||||
* 微信小程序 WXSS 不支持以下 CSS 特性:
|
||||
* 1. 反斜杠转义的特殊字符(如 .\!visible)—— !important 修饰符
|
||||
* 2. :not() 伪类 + ~ 兄弟选择器组合(如 .space-y-4 > :not([hidden]) ~ :not([hidden]))
|
||||
* 3. CSS 自定义属性 / CSS 变量(如 --tw-space-y-reverse)
|
||||
* 4. 嵌套 calc()(如 calc(0.5rem * calc(1 - var(--tw-space-y-reverse))))
|
||||
*
|
||||
* 该插件移除以下类型的 Tailwind CSS 规则:
|
||||
* - .\!xxx 类选择器(important 修饰符)
|
||||
* - .space-[x|y]-* 规则(间距布局,依赖 :not + ~ + CSS 变量 + 嵌套 calc)
|
||||
* - .divide-[x|y]-* 规则(分割线布局,同样依赖不兼容特性)
|
||||
*/
|
||||
|
||||
/** @type {import('postcss').PluginCreator} */
|
||||
module.exports = (options = {}) => {
|
||||
return {
|
||||
postcssPlugin: 'postcss-remove-wxss-incompatible',
|
||||
Rule(rule) {
|
||||
const selector = rule.selector
|
||||
|
||||
// 1) 移除 .\!xxx 类选择器(Tailwind important 修饰符,反斜杠转义的 ! 不兼容 WXSS)
|
||||
const selectors = selector.split(',')
|
||||
const hasEscapedImportant = selectors.some(s => /\.\\!/.test(s.trim()))
|
||||
if (hasEscapedImportant) {
|
||||
rule.remove()
|
||||
return
|
||||
}
|
||||
|
||||
// 2) 移除 .space-[x|y]-* 规则(依赖 :not + ~ 兄弟组合器,WXSS 不兼容)
|
||||
if (/\.space-[xy]-/.test(selector)) {
|
||||
rule.remove()
|
||||
return
|
||||
}
|
||||
|
||||
// 3) 移除 .divide-[x|y]-* 规则(同上,依赖 :not + ~)
|
||||
if (/\.divide-[xy]-/.test(selector)) {
|
||||
rule.remove()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.postcss = true
|
||||
@@ -1,7 +1,12 @@
|
||||
const wxssIncompatiblePlugin = require('./postcss-remove-wxss-incompatible')
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'postcss-nested': {}
|
||||
}
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
require('postcss-nested'),
|
||||
// 移除 WXSS 不兼容的 Tailwind CSS 规则
|
||||
// 包括 .\!xxx(反斜杠转义 !)、space-x/y-*(:not + ~ 组合器)、divide-x/y-* 等
|
||||
wxssIncompatiblePlugin()
|
||||
]
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ const QRScanModal: React.FC<QRScanModalProps> = ({
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<View className="space-y-2">
|
||||
<View className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
|
||||
@@ -126,7 +126,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
|
||||
// 渲染统计概览
|
||||
const renderStatsOverview = () => (
|
||||
<View className="px-4 space-y-4">
|
||||
<View className="px-4 flex flex-col gap-4">
|
||||
{/* 核心数据卡片 */}
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
@@ -182,7 +182,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请来源分析</Text>
|
||||
<View className="space-y-3">
|
||||
<View className="flex flex-col gap-3">
|
||||
{inviteStats.sourceStats.map((source, index) => (
|
||||
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<View className="flex items-center">
|
||||
@@ -208,7 +208,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
const renderInviteRecords = () => (
|
||||
<View className="px-4">
|
||||
{inviteRecords.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
<View className="flex flex-col gap-3">
|
||||
{inviteRecords.map((record, index) => (
|
||||
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4">
|
||||
@@ -253,7 +253,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{ranking.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
<View className="flex flex-col gap-3">
|
||||
{ranking.map((item, index) => (
|
||||
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4 flex items-center">
|
||||
|
||||
@@ -377,7 +377,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* 推广说明 */}
|
||||
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
|
||||
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
||||
<View className="space-y-2">
|
||||
<View className="flex flex-col gap-2">
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
@@ -408,7 +408,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* ) : inviteStats ? (*/}
|
||||
{/* <View className="space-y-4">*/}
|
||||
{/* <View className="flex flex-col gap-4">*/}
|
||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-blue-500">*/}
|
||||
@@ -443,7 +443,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
|
||||
{/* <View className="mt-4">*/}
|
||||
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
|
||||
{/* <View className="space-y-2">*/}
|
||||
{/* <View className="flex flex-col gap-2">*/}
|
||||
{/* {inviteStats.sourceStats.map((source, index) => (*/}
|
||||
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
|
||||
{/* <View className="flex items-center">*/}
|
||||
|
||||
@@ -241,7 +241,7 @@ const PayPage: React.FC = () => {
|
||||
<Divider />
|
||||
|
||||
<View className="flex items-start px-4 py-2">
|
||||
<Tips className="text-orange-500 mr-2 mt-0.5" size="16" />
|
||||
<Tips className="text-orange-500 mr-2 mt-1" size="16" />
|
||||
<Text className="text-sm text-gray-500">
|
||||
支付成功后即可在「已购产品」中使用该应用
|
||||
</Text>
|
||||
|
||||
@@ -83,7 +83,7 @@ const ThemeSelector: React.FC = () => {
|
||||
>
|
||||
<Text className="text-lg font-bold mb-2">当前主题预览</Text>
|
||||
<Text className="text-sm opacity-90 px-2">{currentTheme.description}</Text>
|
||||
<View className="mt-4 flex justify-center space-x-4">
|
||||
<View className="mt-4 flex justify-center">
|
||||
<View
|
||||
className="w-8 h-8 rounded-full"
|
||||
style={{ backgroundColor: currentTheme.primary }}
|
||||
|
||||
@@ -50,11 +50,15 @@ export function copyText(text: string) {
|
||||
Taro.setClipboardData({
|
||||
data: text,
|
||||
success: function () {
|
||||
Taro.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
// 微信小程序调用 setClipboardData 成功后,系统会自带「内容已复制」提示。
|
||||
// 为避免重复 Toast 影响用户体验,小程序端不再额外显示自定义 Toast。
|
||||
if (process.env.TARO_ENV !== 'weapp') {
|
||||
Taro.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: function () {
|
||||
Taro.showToast({
|
||||
|
||||
Reference in New Issue
Block a user