feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则

- 实现 postcss-remove-wxss-incompatible 插件
- 移除反斜杠转义的重要性修饰符类选择器(.\!xxx)
- 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则
- 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则
- 解决微信小程序 WXSS 不支持的 CSS 特性问题
This commit is contained in:
2026-06-30 16:38:54 +08:00
parent e4524154e0
commit 5f71295aae
9 changed files with 114 additions and 20 deletions

View File

@@ -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 规范。

View 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

View File

@@ -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()
]
}

View File

@@ -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"

View File

@@ -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">

View File

@@ -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">*/}

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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({