diff --git a/.browserslistrc b/.browserslistrc
new file mode 100644
index 0000000..b16dd4f
--- /dev/null
+++ b/.browserslistrc
@@ -0,0 +1,7 @@
+# 兼容国产手机、微信内置浏览器、旧版 Android WebView
+>= 0.5%
+last 2 versions
+not dead
+Chrome >= 64
+Android >= 5
+iOS >= 11
diff --git a/.env b/.env
index 1e1fbb4..f47558f 100644
--- a/.env
+++ b/.env
@@ -4,6 +4,7 @@
# VITE_USE_MOCK_DATA=false # 全部关闭 mock
# SSH 部署(npm run deploy),可选覆盖
+VITE_USE_MOCK_WALLET=false # 关闭钱包 mock
# DEPLOY_HOST=38.246.250.238
# DEPLOY_USER=root
# DEPLOY_PATH=/opt/1panel/www/sites/pm.xtrader.vip/index
diff --git a/docs/views/Home.md b/docs/views/Home.md
index 2afaf64..34ef538 100644
--- a/docs/views/Home.md
+++ b/docs/views/Home.md
@@ -8,10 +8,10 @@
## 核心能力
-- **分类导航**:三层级分类选择(一级 Tab、二级图标、三级 Tab)
+- **分类导航**:三层级分类选择(一级 Tab、二级文字标签、三级 Tab)
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
- **搜索**:可按关键词搜索事件
-- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选
+- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选;切换语言时重新请求分类接口并刷新列表
- **Footer**:使用 `` 组件,包含品牌、链接、语言选择、免责声明
## 数据流
diff --git a/package-lock.json b/package-lock.json
index f6ae80f..3e93f9e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
+ "autoprefixer": "^10.4.27",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.42.0",
@@ -40,6 +41,7 @@
"jsdom": "^27.4.0",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.42.0",
+ "postcss": "^8.5.8",
"prettier": "3.8.1",
"typescript": "~5.9.3",
"vite": "^7.3.1",
@@ -3307,6 +3309,43 @@
"url": "https://github.com/sponsors/sxzz"
}
},
+ "node_modules/autoprefixer": {
+ "version": "10.4.27",
+ "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.27.tgz",
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001774",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -3715,9 +3754,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001767",
- "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz",
- "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==",
+ "version": "1.0.30001780",
+ "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
+ "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
"dev": true,
"funding": [
{
@@ -5042,6 +5081,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
@@ -6867,9 +6920,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "version": "8.5.8",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"funding": [
{
"type": "opencollective",
@@ -6908,6 +6961,13 @@
"node": ">=4"
}
},
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
diff --git a/package.json b/package.json
index c34ff0c..54b512d 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
+ "autoprefixer": "^10.4.27",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.42.0",
@@ -50,6 +51,7 @@
"jsdom": "^27.4.0",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.42.0",
+ "postcss": "^8.5.8",
"prettier": "3.8.1",
"typescript": "~5.9.3",
"vite": "^7.3.1",
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..b6dc034
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ autoprefixer: {},
+ },
+}
diff --git a/src/composables/useLightweightChart.ts b/src/composables/useLightweightChart.ts
index fa28a31..af8a8c9 100644
--- a/src/composables/useLightweightChart.ts
+++ b/src/composables/useLightweightChart.ts
@@ -39,8 +39,9 @@ export function toLwcData(points: [number, number][]): { time: UTCTimestamp; val
for (const [t, v] of sorted) {
const utcSec = t >= 1e12 ? t / 1000 : t
const time = timeToLocal(utcSec)
- if (result.length > 0 && result[result.length - 1].time === time) {
- result[result.length - 1].value = v
+ const last = result[result.length - 1]
+ if (last && last.time === time) {
+ last.value = v
} else {
result.push({ time, value: v })
}
diff --git a/src/views/Home.vue b/src/views/Home.vue
index f7b3153..26148ad 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -116,13 +116,6 @@
:class="{ 'home-category-icon-item--active': layerActiveValues[1] === item.id }"
@click="onCategorySelect(1, item.id)"
>
-
- {{ item.icon || DEFAULT_CATEGORY_ICON }}
-
{{ item.label }}
@@ -243,11 +236,9 @@ import {
type EventCardItem,
} from '../api/event'
import {
- DEFAULT_CATEGORY_ICON,
enrichWithIcons,
getPmTagMain,
MOCK_CATEGORY_TREE,
- resolveCategoryIconColor,
type CategoryTreeNode,
} from '../api/category'
import { USE_MOCK_CATEGORY } from '../config/mock'
@@ -480,13 +471,49 @@ function onCardOpenTrade(
const toastStore = useToastStore()
const localeStore = useLocaleStore()
-// 监听语言切换,语言变化时重新加载事件列表
+/** 加载分类树(接口或 mock),完成后初始化选中并触发事件列表加载 */
+async function loadCategory() {
+ function loadEventListAfterCategoryReady() {
+ const cached = getEventListCache()
+ if (cached && cached.list.length > 0) {
+ eventList.value = cached.list
+ eventPage.value = cached.page
+ eventTotal.value = cached.total
+ eventPageSize.value = cached.pageSize
+ } else {
+ loadEvents(1, false)
+ }
+ }
+
+ if (USE_MOCK_CATEGORY) {
+ categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
+ initCategorySelection()
+ loadEventListAfterCategoryReady()
+ } else {
+ try {
+ const res = await getPmTagMain()
+ if (res.code === 0 || res.code === 200) {
+ const data = res.data
+ categoryTree.value =
+ Array.isArray(data) && data.length > 0 ? data : enrichWithIcons(MOCK_CATEGORY_TREE)
+ } else {
+ categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
+ }
+ } catch {
+ categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
+ }
+ initCategorySelection()
+ loadEventListAfterCategoryReady()
+ }
+}
+
+// 监听语言切换,语言变化时重新请求分类并加载事件列表
watch(
() => localeStore.currentLocale,
() => {
clearEventListCache()
eventPage.value = 1
- loadEvents(1, false, activeSearchKeyword.value)
+ loadCategory()
},
)
@@ -605,41 +632,7 @@ function checkScrollLoad() {
}
onMounted(() => {
- /** 分类树就绪后加载列表(确保 activeTagIds 已计算,与下拉刷新参数一致) */
- function loadEventListAfterCategoryReady() {
- const cached = getEventListCache()
- if (cached && cached.list.length > 0) {
- eventList.value = cached.list
- eventPage.value = cached.page
- eventTotal.value = cached.total
- eventPageSize.value = cached.pageSize
- } else {
- loadEvents(1, false)
- }
- }
-
- if (USE_MOCK_CATEGORY) {
- categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
- initCategorySelection()
- loadEventListAfterCategoryReady()
- } else {
- getPmTagMain()
- .then((res) => {
- if (res.code === 0 || res.code === 200) {
- const data = res.data
- categoryTree.value = Array.isArray(data) && data.length > 0 ? data : enrichWithIcons(MOCK_CATEGORY_TREE)
- } else {
- categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
- }
- initCategorySelection()
- loadEventListAfterCategoryReady()
- })
- .catch(() => {
- categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
- initCategorySelection()
- loadEventListAfterCategoryReady()
- })
- }
+ loadCategory()
nextTick(() => {
const sentinel = sentinelRef.value
if (sentinel) {
@@ -996,15 +989,15 @@ onActivated(() => {
}
.home-category-layer--icon {
- padding: 12px 16px;
+ padding: 6px 16px;
}
.home-category-icon-row {
display: flex;
flex-wrap: nowrap;
- gap: 8px;
+ gap: 6px;
overflow-x: auto;
- padding-bottom: 4px;
+ padding-bottom: 2px;
scrollbar-width: none;
}
@@ -1015,13 +1008,12 @@ onActivated(() => {
.home-category-icon-item {
flex-shrink: 0;
display: flex;
- flex-direction: column;
align-items: center;
- gap: 4px;
- min-width: 56px;
- padding: 8px 12px;
+ justify-content: center;
+ min-width: 44px;
+ padding: 4px 10px;
border: none;
- border-radius: 8px;
+ border-radius: 6px;
background: transparent;
color: #64748b;
font-size: 12px;
diff --git a/vite.config.ts b/vite.config.ts
index 0273622..2fae3c8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -3,6 +3,16 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
+/** 移除 crossorigin 属性,避免部分手机浏览器加载资源异常 */
+function removeCrossorigin() {
+ return {
+ name: 'remove-crossorigin',
+ transformIndexHtml(html: string) {
+ return html.replace(/\s*crossorigin\s*/g, ' ')
+ },
+ }
+}
+
export default defineConfig({
resolve: {
alias: {
@@ -11,17 +21,19 @@ export default defineConfig({
},
plugins: [
vue(),
- // 2. 将此插件添加到插件数组中
nodePolyfills({
- // 为了彻底解决SIWE等库的问题,建议包含以下选项
protocolImports: true,
}),
+ removeCrossorigin(),
],
- // 3. 可选但推荐:显式定义 process.env 以避免其他潜在错误
define: {
'process.env': {},
},
+ build: {
+ target: 'es2020',
+ cssTarget: 'chrome64',
+ },
server: {
- host: true, // 监听 0.0.0.0,允许局域网内其他设备访问
+ host: true,
},
})