init pages
This commit is contained in:
parent
0477c632b4
commit
f633d59fd2
443
package-lock.json
generated
443
package-lock.json
generated
@ -2733,6 +2733,70 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"cpu": [
|
||||
@ -2747,6 +2811,294 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.1.9",
|
||||
"license": "MIT",
|
||||
@ -7284,15 +7636,6 @@
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/growly": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/growly/-/growly-1.3.0.tgz",
|
||||
"integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@ -7599,24 +7942,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -7704,21 +8029,6 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isbinaryfile": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.4.tgz",
|
||||
@ -9151,36 +9461,6 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-notifier": {
|
||||
"version": "9.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"growly": "^1.3.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"semver": "^7.3.2",
|
||||
"shellwords": "^0.1.1",
|
||||
"uuid": "^8.3.0",
|
||||
"which": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/node-notifier/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz",
|
||||
@ -10599,15 +10879,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shellwords": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/shellwords/-/shellwords-0.1.1.tgz",
|
||||
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@ -11545,16 +11816,6 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "8.1.1",
|
||||
"dev": true,
|
||||
|
||||
78
src/components/app/AppNoticeBar.vue
Normal file
78
src/components/app/AppNoticeBar.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<view class="app-notice-board" v-if="visible">
|
||||
<view class="app-notice-icon">
|
||||
<tui-icon name="news-fill" :size="24" color="#f54f46"></tui-icon>
|
||||
</view>
|
||||
<view class="app-notice-scroll">
|
||||
<view class="app-notice-text" :class="{ 'app-notice-animation': animation }">{{ text }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TuiIcon from "@/components/thorui/tui-icon/tui-icon.vue";
|
||||
|
||||
defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
animation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-notice-board {
|
||||
width: 100%;
|
||||
padding-right: 30rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: 28rpx;
|
||||
height: 60rpx;
|
||||
background: #fff8d5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.app-notice-icon {
|
||||
background: #fff8d5;
|
||||
padding-left: 30rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.app-notice-scroll {
|
||||
flex: 1;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #f54f46;
|
||||
}
|
||||
|
||||
.app-notice-text {
|
||||
-webkit-backface-visibility: hidden;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
|
||||
.app-notice-animation {
|
||||
-webkit-animation: app-notice-rolling 12s linear infinite;
|
||||
animation: app-notice-rolling 12s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes app-notice-rolling {
|
||||
0% {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(-170%, 0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
src/components/app/InlineState.vue
Normal file
78
src/components/app/InlineState.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<view class="state-card" :class="type">
|
||||
<text class="state-text">{{ text }}</text>
|
||||
<button v-if="retryText" class="retry-button" @tap="$emit('retry')">{{ retryText }}</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits(["retry"]);
|
||||
|
||||
defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "loading",
|
||||
},
|
||||
retryText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.state-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
margin: 15px 0;
|
||||
padding: 30px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
content: "";
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #4f7cfe;
|
||||
animation: spin 1s linear infinite;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: #4f7cfe;
|
||||
background: #f1f5fe;
|
||||
border-radius: 20px;
|
||||
padding: 6px 18px;
|
||||
}
|
||||
|
||||
.retry-button::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
src/components/app/UsageDescription.vue
Normal file
36
src/components/app/UsageDescription.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<view class="usage-description">
|
||||
<text class="usage-text" v-for="(tip, index) in tips" :key="tip">{{ index + 1 }}. {{ tip }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
tips: {
|
||||
type: Array as () => string[],
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.usage-description {
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 30rpx;
|
||||
margin-top: 30rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.usage-text {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #777;
|
||||
line-height: 1.65;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.usage-text:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
194
src/components/home/HomeFeatureCard.vue
Normal file
194
src/components/home/HomeFeatureCard.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<view class="function-card">
|
||||
<view class="function-left">
|
||||
<view class="icon-wrapper" :class="iconClass">
|
||||
<image class="function-icon" :src="iconPath" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="function-text">
|
||||
<view class="title-row">
|
||||
<text class="function-title">{{ title }}</text>
|
||||
<text class="feature-tag" v-if="tag">{{ tag }}</text>
|
||||
</view>
|
||||
<text class="function-description">{{ description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="function-right">
|
||||
<button class="function-button" :class="buttonClass" hover-class="button-hover" @tap="$emit('action')">
|
||||
<view class="button-content">
|
||||
<text>{{ buttonText }}</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits(["action"]);
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
buttonClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.function-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(255, 250, 253, 0.86)),
|
||||
linear-gradient(90deg, rgba(255, 204, 220, 0.2), rgba(207, 237, 255, 0.16));
|
||||
margin: 0 0 20rpx;
|
||||
padding: 28rpx 26rpx;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(83, 96, 130, 0.08);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.86);
|
||||
backdrop-filter: blur(12rpx);
|
||||
}
|
||||
|
||||
.function-card:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.function-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.function-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 18rpx;
|
||||
margin-right: 22rpx;
|
||||
background: linear-gradient(145deg, #fff, #f2f7ff);
|
||||
padding: 14rpx;
|
||||
box-shadow: 0 10rpx 22rpx rgba(112, 126, 157, 0.1);
|
||||
}
|
||||
|
||||
.function-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.function-title {
|
||||
font-size: 31rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.feature-tag {
|
||||
margin-left: 12rpx;
|
||||
padding: 5rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 232, 240, 0.86);
|
||||
color: #d94f75;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.2;
|
||||
flex-shrink: 0;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.function-description {
|
||||
font-size: 24rpx;
|
||||
color: #7b8493;
|
||||
display: block;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.function-button {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.function-button::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.button-content {
|
||||
background-color: #f1f5fe;
|
||||
color: #4f7cfe;
|
||||
padding: 13rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-left: 6rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.color-button .button-content {
|
||||
background: linear-gradient(135deg, #ff6f96 0%, #77b9ff 100%);
|
||||
color: white;
|
||||
box-shadow: 0 10rpx 20rpx rgba(255, 111, 150, 0.18);
|
||||
}
|
||||
|
||||
.green-button .button-content {
|
||||
background: rgba(236, 249, 244, 0.9);
|
||||
color: #179b70;
|
||||
}
|
||||
|
||||
.purple-button .button-content {
|
||||
background: rgba(244, 240, 255, 0.9);
|
||||
color: #7861d4;
|
||||
}
|
||||
|
||||
.red-button .button-content {
|
||||
background: rgba(255, 241, 245, 0.92);
|
||||
color: #d94f75;
|
||||
}
|
||||
</style>
|
||||
240
src/components/home/HomeHeroCard.vue
Normal file
240
src/components/home/HomeHeroCard.vue
Normal file
@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<view class="hero">
|
||||
<view class="hero-copy">
|
||||
<text class="hero-eyebrow">{{ eyebrow }}</text>
|
||||
<text class="hero-title">{{ title }}</text>
|
||||
<text class="hero-subtitle">{{ subtitle }}</text>
|
||||
|
||||
<view class="hero-tags">
|
||||
<text class="hero-tag" v-for="item in highlights" :key="item">{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="hero-visual">
|
||||
<image class="hero-image" :src="image" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<view class="hero-actions">
|
||||
<button class="primary-button" @tap="$emit('action')">{{ actionText }}</button>
|
||||
<button class="secondary-button" @tap="$emit('secondary')">{{ secondaryText }}</button>
|
||||
</view>
|
||||
|
||||
<view class="sparkle sparkle-one"></view>
|
||||
<view class="sparkle sparkle-two"></view>
|
||||
<view class="sparkle sparkle-three"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits(["action", "secondary"]);
|
||||
|
||||
defineProps({
|
||||
eyebrow: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
highlights: {
|
||||
type: Array as () => string[],
|
||||
required: true,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
secondaryText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 34rpx;
|
||||
padding: 38rpx 32rpx 30rpx;
|
||||
background:
|
||||
linear-gradient(140deg, rgba(255, 247, 252, 0.96) 0%, rgba(255, 255, 255, 0.9) 38%, rgba(232, 247, 255, 0.92) 100%),
|
||||
linear-gradient(45deg, rgba(255, 180, 206, 0.36), rgba(153, 217, 255, 0.2));
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 22rpx 52rpx rgba(89, 101, 132, 0.12);
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 28rpx;
|
||||
right: 28rpx;
|
||||
top: 24rpx;
|
||||
height: 1rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.96), transparent);
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -80rpx;
|
||||
bottom: -120rpx;
|
||||
width: 360rpx;
|
||||
height: 320rpx;
|
||||
background: linear-gradient(150deg, rgba(255, 216, 228, 0.46), rgba(198, 235, 255, 0.28));
|
||||
transform: rotate(-16deg);
|
||||
border-radius: 42% 58% 48% 52%;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 64%;
|
||||
}
|
||||
|
||||
.hero-eyebrow {
|
||||
display: block;
|
||||
color: #d94f75;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: #20273a;
|
||||
font-size: 46rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: #626b7b;
|
||||
font-size: 27rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.hero-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.hero-tag {
|
||||
margin-right: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 9rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: #6c7280;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.2;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 8rpx 20rpx rgba(124, 143, 171, 0.08);
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
position: absolute;
|
||||
right: 10rpx;
|
||||
top: 34rpx;
|
||||
width: 238rpx;
|
||||
height: 226rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 16rpx 26rpx rgba(230, 95, 131, 0.12));
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.primary-button,
|
||||
.secondary-button {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.primary-button::after,
|
||||
.secondary-button::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff6f96 0%, #ff9a7a 100%);
|
||||
box-shadow: 0 14rpx 28rpx rgba(255, 111, 150, 0.28);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
width: 190rpx;
|
||||
margin-left: 18rpx;
|
||||
color: #d94f75;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 10rpx 24rpx rgba(124, 143, 171, 0.08);
|
||||
}
|
||||
|
||||
.sparkle {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
background: #fff;
|
||||
transform: rotate(45deg);
|
||||
box-shadow: 0 0 18rpx rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.sparkle-one {
|
||||
top: 34rpx;
|
||||
left: 58%;
|
||||
}
|
||||
|
||||
.sparkle-two {
|
||||
top: 164rpx;
|
||||
right: 42rpx;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
}
|
||||
|
||||
.sparkle-three {
|
||||
bottom: 108rpx;
|
||||
left: 40rpx;
|
||||
width: 7rpx;
|
||||
height: 7rpx;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
.hero-copy {
|
||||
width: 68%;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
width: 190rpx;
|
||||
height: 190rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
232
src/components/mail/MailCard.vue
Normal file
232
src/components/mail/MailCard.vue
Normal file
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<view
|
||||
class="mail-card"
|
||||
:class="[statusMeta.className, { 'new-card': mail.isNew }]"
|
||||
hover-class="card-hover"
|
||||
@tap="$emit('detail', mail)"
|
||||
>
|
||||
<view class="card-glow"></view>
|
||||
|
||||
<view class="card-header tui-skeleton-rect">
|
||||
<view class="status-badge" :class="statusMeta.className">
|
||||
<text class="status-dot"></text>
|
||||
<text>{{ statusMeta.text }}</text>
|
||||
</view>
|
||||
<text class="order-number">#{{ mail.orderNumber }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-content">
|
||||
<view class="recipient-row">
|
||||
<text class="label-text">联系人</text>
|
||||
<text class="recipient-value tui-skeleton-rect">{{ maskedRecipient }}</text>
|
||||
</view>
|
||||
|
||||
<view class="message-bubble tui-skeleton-rect">
|
||||
<text class="content-value">{{ mail.content }}</text>
|
||||
</view>
|
||||
|
||||
<view class="time-row tui-skeleton-rect">
|
||||
<text class="time-label">{{ mail.status === sentStatus ? "发送" : "提交" }}于</text>
|
||||
<text class="time-value">{{ displayTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { MailStatus, MAIL_STATUS_META, formatMailTime, maskRecipient, type Mail } from "@/config/mail";
|
||||
|
||||
const props = defineProps({
|
||||
mail: {
|
||||
type: Object as () => Mail,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(["detail"]);
|
||||
|
||||
const sentStatus = MailStatus.SENT;
|
||||
|
||||
const statusMeta = computed(() => MAIL_STATUS_META[props.mail.status]);
|
||||
|
||||
const maskedRecipient = computed(() => maskRecipient(props.mail.recipient));
|
||||
|
||||
const displayTime = computed(() => {
|
||||
const time = props.mail.status === MailStatus.SENT ? props.mail.sendTime : props.mail.submitTime;
|
||||
return formatMailTime(time);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mail-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 22rpx;
|
||||
padding: 22rpx;
|
||||
border-radius: 26rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.88);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 248, 252, 0.84) 54%, rgba(241, 249, 255, 0.88) 100%),
|
||||
rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.09);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&.card-hover {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 12rpx 26rpx rgba(93, 107, 139, 0.06);
|
||||
}
|
||||
|
||||
&.new-card {
|
||||
animation: fadeSlideIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
top: -88rpx;
|
||||
right: -70rpx;
|
||||
width: 210rpx;
|
||||
height: 210rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 192, 216, 0.26);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes fadeSlideIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(30rpx);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 46rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
|
||||
&.status-pending {
|
||||
background: linear-gradient(135deg, #ffb75e 0%, #f59e0b 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(245, 158, 11, 0.18);
|
||||
}
|
||||
|
||||
&.status-sent {
|
||||
background: linear-gradient(135deg, #7ee0a3 0%, #34b878 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(52, 184, 120, 0.18);
|
||||
}
|
||||
|
||||
&.status-failed {
|
||||
background: linear-gradient(135deg, #ff8daf 0%, #e85b7f 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(232, 91, 127, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 10rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.order-number {
|
||||
flex: 1;
|
||||
color: #9aa3b2;
|
||||
font-size: 22rpx;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: 22rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recipient-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
flex-shrink: 0;
|
||||
height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
padding: 0 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: #8c96a8;
|
||||
font-size: 21rpx;
|
||||
margin-right: 14rpx;
|
||||
border: 1rpx solid rgba(232, 238, 248, 0.9);
|
||||
}
|
||||
|
||||
.recipient-value {
|
||||
flex: 1;
|
||||
color: #263044;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 22rpx;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
border: 1rpx solid rgba(242, 231, 239, 0.9);
|
||||
}
|
||||
|
||||
.content-value {
|
||||
display: block;
|
||||
color: #586174;
|
||||
font-size: 27rpx;
|
||||
line-height: 1.65;
|
||||
max-height: 90rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
color: #a0a9b8;
|
||||
font-size: 22rpx;
|
||||
margin-right: 6rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
color: #737d90;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
</style>
|
||||
159
src/components/mail/MailListState.vue
Normal file
159
src/components/mail/MailListState.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<view v-if="loading" class="loading-state">
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="empty" class="empty-state">
|
||||
<view class="empty-card">
|
||||
<image class="empty-image tui-skeleton-rect" :src="emptyImage" mode="aspectFit"></image>
|
||||
<text class="empty-title tui-skeleton-rect">还没有消息</text>
|
||||
<text class="empty-text tui-skeleton-rect">{{ emptyText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="noMore" class="no-more">
|
||||
<view class="no-more-line"></view>
|
||||
<text class="no-more-text">{{ noMoreText }}</text>
|
||||
<view class="no-more-line"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
empty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noMore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyImage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: "暂无短信",
|
||||
},
|
||||
noMoreText: {
|
||||
type: String,
|
||||
default: "已经到底了",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 42rpx 0;
|
||||
height: 42rpx;
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ff92b6, #8bcfff);
|
||||
margin: 0 7rpx;
|
||||
animation: loadingDot 1.4s infinite ease-in-out both;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loadingDot {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 70rpx 24rpx;
|
||||
min-height: 58vh;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 58rpx 34rpx 62rpx;
|
||||
border-radius: 28rpx;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.86), rgba(247, 252, 255, 0.74)),
|
||||
rgba(255, 255, 255, 0.72);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.08);
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
width: 330rpx;
|
||||
height: 230rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
color: #263044;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #8c96a8;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 0 34rpx;
|
||||
}
|
||||
|
||||
.no-more-line {
|
||||
height: 1rpx;
|
||||
width: 120rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(153, 170, 190, 0.35));
|
||||
|
||||
&:last-child {
|
||||
background: linear-gradient(90deg, rgba(153, 170, 190, 0.35), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.no-more-text {
|
||||
color: #9aa3b2;
|
||||
font-size: 22rpx;
|
||||
margin: 0 18rpx;
|
||||
}
|
||||
</style>
|
||||
187
src/components/planning/PlanCard.vue
Normal file
187
src/components/planning/PlanCard.vue
Normal file
@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="plan-card" hover-class="card-hover">
|
||||
<view class="card-glow"></view>
|
||||
|
||||
<view class="card-top">
|
||||
<view class="cycle-pill">
|
||||
<text>{{ plan.cycleName }}</text>
|
||||
</view>
|
||||
<view class="status-dot" :class="statusMeta.className"></view>
|
||||
</view>
|
||||
|
||||
<view class="account-row">
|
||||
<text class="account-label">{{ plan.socialTypeName }}</text>
|
||||
<text class="account-value">{{ maskedAccount }}</text>
|
||||
</view>
|
||||
|
||||
<view class="content-box">
|
||||
<text class="content-text">{{ plan.content }}</text>
|
||||
</view>
|
||||
|
||||
<view class="meta-row">
|
||||
<text class="meta-label">下次传送</text>
|
||||
<text class="meta-value">{{ nextRunTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { formatMailTime } from "@/config/mail";
|
||||
import { PLAN_STATUS_META, maskSocialAccount, type PlanItem } from "@/config/planning";
|
||||
|
||||
const props = defineProps({
|
||||
plan: {
|
||||
type: Object as () => PlanItem,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const statusMeta = computed(() => PLAN_STATUS_META[props.plan.status]);
|
||||
const maskedAccount = computed(() => maskSocialAccount(props.plan.socialAccount));
|
||||
const nextRunTime = computed(() => formatMailTime(props.plan.nextRunTime));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.plan-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 22rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.76), rgba(247, 252, 255, 0.54)),
|
||||
rgba(255, 255, 255, 0.52);
|
||||
box-shadow: 0 12rpx 28rpx rgba(93, 107, 139, 0.06);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 12rpx 26rpx rgba(93, 107, 139, 0.06);
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
top: -72rpx;
|
||||
right: -66rpx;
|
||||
width: 170rpx;
|
||||
height: 170rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 192, 216, 0.18);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-top,
|
||||
.account-row,
|
||||
.content-box,
|
||||
.meta-row {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.cycle-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 21rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cycle-pill {
|
||||
color: #d94f75;
|
||||
background: rgba(255, 232, 241, 0.86);
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #35b779;
|
||||
}
|
||||
|
||||
.status-done {
|
||||
background: #9aa3b2;
|
||||
}
|
||||
|
||||
.status-paused {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 8rpx rgba(154, 163, 178, 0.12);
|
||||
}
|
||||
|
||||
.account-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.account-label {
|
||||
height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
padding: 0 12rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #8c96a8;
|
||||
font-size: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1rpx solid rgba(232, 238, 248, 0.9);
|
||||
}
|
||||
|
||||
.account-value {
|
||||
margin-left: 12rpx;
|
||||
color: #263044;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
margin-top: 14rpx;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
display: block;
|
||||
color: #586174;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
flex-shrink: 0;
|
||||
color: #a0a9b8;
|
||||
font-size: 21rpx;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #737d90;
|
||||
font-size: 21rpx;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
107
src/components/profile/ProfileHeader.vue
Normal file
107
src/components/profile/ProfileHeader.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<view class="header">
|
||||
<view class="profile-info">
|
||||
<view class="profile-pic-container">
|
||||
<image :src="avatar" mode="aspectFill" class="profile-pic"></image>
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<text class="username">{{ username }}</text>
|
||||
<view class="id-copy-wrapper">
|
||||
<text class="user-id">ID: {{ userId }}</text>
|
||||
<text class="copy-id" @tap="$emit('copy-id')">复制ID</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits(["copy-id"]);
|
||||
|
||||
defineProps({
|
||||
avatar: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
width: 100%;
|
||||
padding: 30rpx;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.86), rgba(247, 252, 255, 0.66)),
|
||||
rgba(255, 255, 255, 0.72);
|
||||
margin-bottom: 22rpx;
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.08);
|
||||
flex-shrink: 0;
|
||||
border-radius: 28rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-pic-container {
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
border-radius: 50%;
|
||||
padding: 6rpx;
|
||||
background: linear-gradient(135deg, rgba(255, 120, 162, 0.18), rgba(131, 198, 255, 0.18));
|
||||
box-shadow: 0 12rpx 30rpx rgba(93, 107, 139, 0.1);
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 4rpx solid #fff;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 36rpx;
|
||||
color: #20273a;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.id-copy-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 24rpx;
|
||||
color: #7a8496;
|
||||
}
|
||||
|
||||
.copy-id {
|
||||
color: #d94f75;
|
||||
font-size: 23rpx;
|
||||
margin-left: 12rpx;
|
||||
padding: 5rpx 12rpx;
|
||||
background: rgba(255, 232, 241, 0.72);
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
</style>
|
||||
82
src/components/profile/ProfileMenuSection.vue
Normal file
82
src/components/profile/ProfileMenuSection.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<view class="menu-section">
|
||||
<view
|
||||
class="menu-item"
|
||||
hover-class="menu-item-hover"
|
||||
v-for="item in items"
|
||||
:key="item.label"
|
||||
@tap="$emit('select', item)"
|
||||
>
|
||||
<text class="item-text" :class="{ highlight: item.highlight }">{{ item.label }}</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface ProfileMenuItem {
|
||||
label: string;
|
||||
route?: string;
|
||||
action?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
defineEmits(["select"]);
|
||||
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array as () => ProfileMenuItem[],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-section {
|
||||
width: 100%;
|
||||
margin-bottom: 22rpx;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.84), rgba(247, 252, 255, 0.62)),
|
||||
rgba(255, 255, 255, 0.7);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border-radius: 26rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 16rpx 36rpx rgba(93, 107, 139, 0.07);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 92rpx;
|
||||
padding: 0 26rpx;
|
||||
border-bottom: 1rpx solid rgba(230, 236, 246, 0.72);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-item-hover {
|
||||
background-color: rgba(255, 255, 255, 0.54);
|
||||
}
|
||||
|
||||
.item-text {
|
||||
font-size: 28rpx;
|
||||
color: #263044;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #d94f75;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #a7b0bf;
|
||||
font-size: 42rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
203
src/components/reply/ReplyCard.vue
Normal file
203
src/components/reply/ReplyCard.vue
Normal file
@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<view class="reply-card" :class="{ unread: reply.unread }" hover-class="card-hover" @tap="$emit('open', reply)">
|
||||
<view class="card-glow"></view>
|
||||
|
||||
<view class="card-top">
|
||||
<view class="source-pill" :class="sourceMeta.className">
|
||||
<text class="source-dot"></text>
|
||||
<text>{{ sourceMeta.text }}</text>
|
||||
</view>
|
||||
<text class="reply-time">{{ displayTime }}</text>
|
||||
</view>
|
||||
|
||||
<view class="contact-row">
|
||||
<text class="contact-label">来自</text>
|
||||
<text class="contact-phone">{{ maskedPhone }}</text>
|
||||
<text v-if="reply.unread" class="unread-dot"></text>
|
||||
</view>
|
||||
|
||||
<view class="reply-bubble">
|
||||
<text class="reply-content">{{ reply.replyMessage }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-footer">
|
||||
<text class="original-label">原短信</text>
|
||||
<text class="original-text">{{ reply.originalMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { formatMailTime, maskRecipient } from "@/config/mail";
|
||||
import { REPLY_SOURCE_META, type ReplyItem } from "@/config/reply";
|
||||
|
||||
const props = defineProps({
|
||||
reply: {
|
||||
type: Object as () => ReplyItem,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(["open"]);
|
||||
|
||||
const sourceMeta = computed(() => REPLY_SOURCE_META[props.reply.source]);
|
||||
const maskedPhone = computed(() => maskRecipient(props.reply.contactPhone));
|
||||
const displayTime = computed(() => formatMailTime(props.reply.replyTime));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reply-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 22rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 28rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.88);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 248, 252, 0.84) 54%, rgba(241, 249, 255, 0.88) 100%),
|
||||
rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.09);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 12rpx 26rpx rgba(93, 107, 139, 0.06);
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
top: -82rpx;
|
||||
right: -72rpx;
|
||||
width: 210rpx;
|
||||
height: 210rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 192, 216, 0.25);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-top,
|
||||
.contact-row,
|
||||
.reply-bubble,
|
||||
.card-footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.source-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 46rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.source-sms {
|
||||
background: linear-gradient(135deg, #ff78a2 0%, #ed6f9b 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(237, 111, 155, 0.18);
|
||||
}
|
||||
|
||||
.source-manual {
|
||||
background: linear-gradient(135deg, #8bcfff 0%, #6caef5 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(108, 174, 245, 0.18);
|
||||
}
|
||||
|
||||
.source-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
margin-right: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
flex: 1;
|
||||
color: #9aa3b2;
|
||||
font-size: 22rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.contact-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.contact-label {
|
||||
height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
padding: 0 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: #8c96a8;
|
||||
font-size: 21rpx;
|
||||
border: 1rpx solid rgba(232, 238, 248, 0.9);
|
||||
}
|
||||
|
||||
.contact-phone {
|
||||
margin-left: 14rpx;
|
||||
color: #263044;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.unread-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
margin-left: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff5d8d;
|
||||
box-shadow: 0 0 0 8rpx rgba(255, 93, 141, 0.12);
|
||||
}
|
||||
|
||||
.reply-bubble {
|
||||
margin-top: 18rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.66);
|
||||
border: 1rpx solid rgba(242, 231, 239, 0.9);
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
display: block;
|
||||
color: #333b4f;
|
||||
font-size: 29rpx;
|
||||
line-height: 1.62;
|
||||
max-height: 94rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.original-label {
|
||||
flex-shrink: 0;
|
||||
color: #a0a9b8;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.original-text {
|
||||
flex: 1;
|
||||
color: #7a8496;
|
||||
font-size: 23rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
58
src/config/form.ts
Normal file
58
src/config/form.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export const APP_NOTICE = {
|
||||
text: "B站10分日本动漫已消失,9.9分仅剩12部,这一部动漫包揽三席!",
|
||||
};
|
||||
|
||||
export const MESSAGE_USAGE_TIPS = [
|
||||
"填写对方手机号码,系统将向该号码发送短信",
|
||||
"可选择未来的发送时间,不选则立即发送",
|
||||
"使用AI文案美化功能可自动生成不同类型的短信内容",
|
||||
"短信按20字一条计费,发送前可预览效果",
|
||||
];
|
||||
|
||||
export const SOCIAL_TYPE_OPTIONS = [
|
||||
{ text: "手机短信", value: 1, inputType: "number" },
|
||||
{ text: "微信", value: 2, inputType: "text" },
|
||||
{ text: "QQ", value: 3, inputType: "number" },
|
||||
{ text: "钉钉", value: 4, inputType: "text" },
|
||||
{ text: "企业微信", value: 5, inputType: "text" },
|
||||
{ text: "其他", value: 6, inputType: "text" },
|
||||
];
|
||||
|
||||
export const MESSAGE_TEMPLATES = [
|
||||
{
|
||||
typeId: 1,
|
||||
type: "表白",
|
||||
desc: "表达喜欢和心意,语气真诚不过度施压",
|
||||
content: "有些话我想认真告诉你:遇见你以后,我开始期待每一次聊天和见面。我喜欢你,也尊重你的想法,只希望这份心意能被你看见。",
|
||||
},
|
||||
{
|
||||
typeId: 2,
|
||||
type: "道歉",
|
||||
desc: "承认问题、表达歉意,减少争辩感",
|
||||
content: "对不起,之前是我没有处理好自己的情绪,也没有站在你的角度考虑。我知道一句道歉不能立刻弥补什么,但我真心希望你能看到我的歉意。",
|
||||
},
|
||||
{
|
||||
typeId: 3,
|
||||
type: "祝福",
|
||||
desc: "适合生日、纪念日和特殊日子的温柔问候",
|
||||
content: "今天想把祝福送给你。愿你接下来的日子顺利、开心,也愿所有美好的事情都慢慢靠近你。",
|
||||
},
|
||||
{
|
||||
typeId: 4,
|
||||
type: "问候",
|
||||
desc: "轻量关心,不打扰也不冒犯",
|
||||
content: "只是想问候你一下,希望你最近一切都好。天气变化记得照顾好自己,也希望你每天都能轻松一点。",
|
||||
},
|
||||
{
|
||||
typeId: 5,
|
||||
type: "晚安",
|
||||
desc: "适合夜晚发送,柔和表达惦记",
|
||||
content: "晚安。希望你今晚能睡个好觉,把今天的疲惫都放下。明天醒来,也能拥有一点新的开心。",
|
||||
},
|
||||
{
|
||||
typeId: 6,
|
||||
type: "思念",
|
||||
desc: "表达想念,但保留体面和边界",
|
||||
content: "有些时刻还是会想起你,想起我们曾经一起经历过的片段。我没有想打扰你,只是想让你知道,我依然珍惜那些回忆。",
|
||||
},
|
||||
];
|
||||
113
src/config/home.ts
Normal file
113
src/config/home.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import callingIcon from "@/assets/1.svg";
|
||||
import manualIcon from "@/assets/2.svg";
|
||||
|
||||
import { APP_ROUTES } from "./routes";
|
||||
|
||||
export interface HomeFeatureCardConfig {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
iconPath: string;
|
||||
buttonText: string;
|
||||
route?: string;
|
||||
buttonClass?: string;
|
||||
iconClass?: string;
|
||||
disabledTip?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export const HOME_HERO = {
|
||||
eyebrow: "匿名短信 · 匿名电话",
|
||||
title: "有些话,换一种方式认真说",
|
||||
subtitle: "被拉黑、想道歉、想告白、想送祝福时,用更克制的方式把心意送达。",
|
||||
highlights: ["匿名代发", "发送前预览", "定时送达"],
|
||||
image: "/static/images/love.png",
|
||||
actionText: "写一条匿名短信",
|
||||
actionRoute: APP_ROUTES.sending,
|
||||
secondaryText: "人工传话",
|
||||
secondaryRoute: APP_ROUTES.manual,
|
||||
};
|
||||
|
||||
export const HOME_PRIMARY_FEATURES: HomeFeatureCardConfig[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "人工传话",
|
||||
description: "真人协助传达,更适合认真道歉和重要告白",
|
||||
iconPath: manualIcon,
|
||||
buttonText: "去传话",
|
||||
route: APP_ROUTES.manual,
|
||||
iconClass: "icon-wrapper-orange",
|
||||
buttonClass: "color-button",
|
||||
tag: "更有温度",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "匿名电话",
|
||||
description: "适合紧急和解、重要解释,先确认意愿再沟通",
|
||||
iconPath: callingIcon,
|
||||
buttonText: "去拨打",
|
||||
route: APP_ROUTES.calling,
|
||||
iconClass: "icon-wrapper-blue",
|
||||
buttonClass: "color-button",
|
||||
disabledTip: "此通道正在维护中,暂时禁用!",
|
||||
tag: "即将开放",
|
||||
},
|
||||
];
|
||||
|
||||
export const HOME_ADDITIONAL_FEATURES: HomeFeatureCardConfig[] = [
|
||||
{
|
||||
id: 3,
|
||||
title: "定时短信",
|
||||
description: "生日、纪念日、晚安问候可提前设置",
|
||||
iconPath: "/static/images/cat.png",
|
||||
buttonText: "去设置",
|
||||
route: APP_ROUTES.planning,
|
||||
tag: "不怕错过",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "AI文案美化",
|
||||
description: "把冲动的话改得更真诚、更体面",
|
||||
iconPath: "/static/images/cat.png",
|
||||
buttonText: "去优化",
|
||||
route: APP_ROUTES.sending,
|
||||
tag: "降低冒犯",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "收到回复",
|
||||
description: "回复统一收进信箱,避免错过对方回应",
|
||||
iconPath: "/static/images/cat.png",
|
||||
buttonText: "去查看",
|
||||
route: APP_ROUTES.reply,
|
||||
tag: "及时查看",
|
||||
},
|
||||
];
|
||||
|
||||
export const HOME_FEATURE_STYLE_CLASSES = {
|
||||
icon: ["icon-wrapper-green", "icon-wrapper-purple", "icon-wrapper-red"],
|
||||
button: ["green-button", "purple-button", "red-button"],
|
||||
};
|
||||
|
||||
export const HOME_SCENARIOS = [
|
||||
{
|
||||
title: "被拉黑后道歉",
|
||||
description: "把重点放在承担责任,不反复打扰。",
|
||||
},
|
||||
{
|
||||
title: "鼓起勇气告白",
|
||||
description: "先表达心意,给对方留出选择空间。",
|
||||
},
|
||||
{
|
||||
title: "生日和纪念日祝福",
|
||||
description: "定时发送,适合不方便直接联系的时刻。",
|
||||
},
|
||||
];
|
||||
|
||||
export const HOME_STEPS = [
|
||||
"选择短信、人工传话或电话",
|
||||
"填写联系人和想说的话",
|
||||
"预览确认后再发送",
|
||||
];
|
||||
|
||||
export const HOME_GUARDRAILS = ["尊重对方意愿", "不鼓励频繁打扰", "内容发送前可检查"];
|
||||
104
src/config/mail.ts
Normal file
104
src/config/mail.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import emptyImage from "@/assets/nodata.svg";
|
||||
|
||||
export enum MailStatus {
|
||||
PENDING = 0,
|
||||
SENT = 1,
|
||||
FAILED = 2,
|
||||
}
|
||||
|
||||
export interface Mail {
|
||||
id: number;
|
||||
orderNumber: string;
|
||||
recipient: string;
|
||||
content: string;
|
||||
sendTime: string;
|
||||
submitTime: string;
|
||||
status: MailStatus;
|
||||
price: number;
|
||||
isNew: boolean;
|
||||
}
|
||||
|
||||
export const MAIL_DETAIL_PAGE = "/pages/mailbox/detail";
|
||||
|
||||
export const MAIL_TABS = [
|
||||
{ name: "全部" },
|
||||
{ name: "待发送" },
|
||||
{ name: "已发送" },
|
||||
{ name: "发送失败" },
|
||||
];
|
||||
|
||||
export const MAIL_EMPTY_IMAGE = emptyImage;
|
||||
|
||||
export const MAIL_STATUS_META: Record<MailStatus, { text: string; className: string }> = {
|
||||
[MailStatus.PENDING]: {
|
||||
text: "待发送",
|
||||
className: "status-pending",
|
||||
},
|
||||
[MailStatus.SENT]: {
|
||||
text: "已发送",
|
||||
className: "status-sent",
|
||||
},
|
||||
[MailStatus.FAILED]: {
|
||||
text: "发送失败",
|
||||
className: "status-failed",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_MAIL_CONTENTS = [
|
||||
"有些话想认真说一次,不打扰你,只希望你知道我的歉意。",
|
||||
"之前没有照顾好你的感受,对不起。愿你之后都能轻松一点。",
|
||||
"如果还有机会,我想把没说好的话认真补上。",
|
||||
"这条短信只是想把心意说清楚,不给你压力。",
|
||||
];
|
||||
|
||||
export const MOCK_PHONE_PREFIXES = ["135", "136", "138", "151", "166", "171", "188", "199"];
|
||||
|
||||
export const maskRecipient = (recipient: string): string => {
|
||||
if (!recipient) return "";
|
||||
|
||||
const value = String(recipient).trim();
|
||||
|
||||
if (value.length <= 7) {
|
||||
return value.replace(/^(.{2}).*(.{1})$/, "$1****$2");
|
||||
}
|
||||
|
||||
return `${value.slice(0, 3)}****${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export const formatMailTime = (timeString: string): string => {
|
||||
if (!timeString || typeof timeString !== "string") {
|
||||
return String(timeString || "");
|
||||
}
|
||||
|
||||
const date = new Date(timeString.replace(" ", "T"));
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return timeString;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
if (year === now.getFullYear()) {
|
||||
return `${month}月${day}日 ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
return `${year}年${month}月${day}日 ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
export const serializeMail = (mail: Mail): string => encodeURIComponent(JSON.stringify(mail));
|
||||
|
||||
export const deserializeMail = (payload?: string): Mail | null => {
|
||||
if (!payload) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(payload)) as Mail;
|
||||
} catch (error) {
|
||||
console.error("解析短信详情失败", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
143
src/config/planning.ts
Normal file
143
src/config/planning.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { SOCIAL_TYPE_OPTIONS } from "@/config/form";
|
||||
import { maskRecipient } from "@/config/mail";
|
||||
|
||||
export type PlanCycleType = "daily" | "weekly" | "monthly";
|
||||
export type PlanStatus = "inProgress" | "completed" | "paused";
|
||||
|
||||
export interface PlanCycleOption {
|
||||
type: PlanCycleType;
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export interface PlanSubmitPayload {
|
||||
cycleType: PlanCycleType;
|
||||
cycleName: string;
|
||||
socialType: number;
|
||||
socialTypeName: string;
|
||||
socialAccount: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface PlanItem {
|
||||
id: string;
|
||||
cycleType: PlanCycleType;
|
||||
cycleName: string;
|
||||
socialTypeName: string;
|
||||
socialAccount: string;
|
||||
content: string;
|
||||
nextRunTime: string;
|
||||
createdAt: string;
|
||||
status: PlanStatus;
|
||||
}
|
||||
|
||||
export const PLAN_CYCLE_OPTIONS: PlanCycleOption[] = [
|
||||
{
|
||||
type: "daily",
|
||||
name: "每天",
|
||||
desc: "每天定时传达一次",
|
||||
},
|
||||
{
|
||||
type: "weekly",
|
||||
name: "每周",
|
||||
desc: "每周固定传达一次",
|
||||
},
|
||||
{
|
||||
type: "monthly",
|
||||
name: "每月",
|
||||
desc: "每月固定传达一次",
|
||||
},
|
||||
];
|
||||
|
||||
export const PLAN_STATUS_TABS = [
|
||||
{ name: "全部", value: "all" },
|
||||
{ name: "进行中", value: "inProgress" },
|
||||
{ name: "已完成", value: "completed" },
|
||||
] as const;
|
||||
|
||||
export const PLAN_STATUS_META: Record<PlanStatus, { text: string; className: string }> = {
|
||||
inProgress: {
|
||||
text: "进行中",
|
||||
className: "status-running",
|
||||
},
|
||||
completed: {
|
||||
text: "已完成",
|
||||
className: "status-done",
|
||||
},
|
||||
paused: {
|
||||
text: "已暂停",
|
||||
className: "status-paused",
|
||||
},
|
||||
};
|
||||
|
||||
export const PLAN_SOCIAL_OPTIONS = SOCIAL_TYPE_OPTIONS.filter((item) => item.value !== 6);
|
||||
|
||||
export const MOCK_PLAN_LIST: PlanItem[] = [
|
||||
{
|
||||
id: "PL250503001",
|
||||
cycleType: "daily",
|
||||
cycleName: "每天",
|
||||
socialTypeName: "手机短信",
|
||||
socialAccount: "19878384602",
|
||||
content: "每天都想轻轻问候你一下,希望你今天也能顺利一点。",
|
||||
nextRunTime: "2026-05-04 20:00:00",
|
||||
createdAt: "2026-05-03 18:30:00",
|
||||
status: "inProgress",
|
||||
},
|
||||
{
|
||||
id: "PL250502002",
|
||||
cycleType: "weekly",
|
||||
cycleName: "每周",
|
||||
socialTypeName: "微信",
|
||||
socialAccount: "moonlight_520",
|
||||
content: "每周给你留一句温柔的话,不催促,只表达我的在意。",
|
||||
nextRunTime: "2026-05-09 21:00:00",
|
||||
createdAt: "2026-05-02 21:12:00",
|
||||
status: "inProgress",
|
||||
},
|
||||
{
|
||||
id: "PL250501003",
|
||||
cycleType: "monthly",
|
||||
cycleName: "每月",
|
||||
socialTypeName: "QQ",
|
||||
socialAccount: "1289045216",
|
||||
content: "这个月也想认真祝你平安顺利,愿你越来越轻松。",
|
||||
nextRunTime: "2026-06-01 09:30:00",
|
||||
createdAt: "2026-05-01 09:18:00",
|
||||
status: "inProgress",
|
||||
},
|
||||
{
|
||||
id: "PL250430004",
|
||||
cycleType: "daily",
|
||||
cycleName: "每天",
|
||||
socialTypeName: "手机短信",
|
||||
socialAccount: "13677268104",
|
||||
content: "晚安,愿你今晚能睡得安心一点。",
|
||||
nextRunTime: "2026-04-30 23:00:00",
|
||||
createdAt: "2026-04-28 22:10:00",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: "PL250428005",
|
||||
cycleType: "weekly",
|
||||
cycleName: "每周",
|
||||
socialTypeName: "企业微信",
|
||||
socialAccount: "yanxi_2026",
|
||||
content: "想把没说好的话慢慢说清楚,也尊重你的节奏。",
|
||||
nextRunTime: "2026-05-05 19:30:00",
|
||||
createdAt: "2026-04-28 16:02:00",
|
||||
status: "paused",
|
||||
},
|
||||
];
|
||||
|
||||
export const maskSocialAccount = (account: string): string => {
|
||||
if (/^1\d{10}$/.test(account)) {
|
||||
return maskRecipient(account);
|
||||
}
|
||||
|
||||
if (account.length <= 6) {
|
||||
return account;
|
||||
}
|
||||
|
||||
return `${account.slice(0, 3)}***${account.slice(-3)}`;
|
||||
};
|
||||
77
src/config/profile.ts
Normal file
77
src/config/profile.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { APP_ROUTES } from "./routes";
|
||||
|
||||
export const PROFILE_INFO = {
|
||||
avatar: "/static/images/cat.png",
|
||||
username: "GT-呆河马",
|
||||
userId: "10086",
|
||||
};
|
||||
|
||||
export const PROFILE_STATS = [
|
||||
{
|
||||
label: "可用余额",
|
||||
value: "¥26.80",
|
||||
},
|
||||
{
|
||||
label: "收到回复",
|
||||
value: "2",
|
||||
route: APP_ROUTES.reply,
|
||||
},
|
||||
{
|
||||
label: "执行计划",
|
||||
value: "3",
|
||||
route: APP_ROUTES.planned,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const PROFILE_QUICK_ACTIONS = [
|
||||
{
|
||||
title: "发匿名短信",
|
||||
desc: "快速写一条短信",
|
||||
icon: "信",
|
||||
route: APP_ROUTES.sending,
|
||||
},
|
||||
{
|
||||
title: "人工传话",
|
||||
desc: "更柔和地表达",
|
||||
icon: "传",
|
||||
route: APP_ROUTES.manual,
|
||||
},
|
||||
{
|
||||
title: "创建计划",
|
||||
desc: "定期传达心意",
|
||||
icon: "计",
|
||||
route: APP_ROUTES.planning,
|
||||
},
|
||||
{
|
||||
title: "我的信箱",
|
||||
desc: "查看发送记录",
|
||||
icon: "箱",
|
||||
route: APP_ROUTES.mailbox,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const PROFILE_MENU_GROUPS = [
|
||||
[
|
||||
{
|
||||
label: "隐私与安全",
|
||||
action: "privacy",
|
||||
},
|
||||
{
|
||||
label: "使用须知",
|
||||
action: "notice",
|
||||
},
|
||||
{
|
||||
label: "联系我们",
|
||||
action: "copyWechat",
|
||||
},
|
||||
],
|
||||
] as const;
|
||||
|
||||
export const CONTACT_CONFIG = {
|
||||
wechatId: "I18888",
|
||||
copySuccessText: "微信号已复制",
|
||||
copyFailText: "复制失败,请重试",
|
||||
};
|
||||
|
||||
export const PROFILE_NOTICE_TEXT = "请真诚表达,避免频繁打扰对方。";
|
||||
export const PROFILE_PRIVACY_TEXT = "平台会保护你的提交信息,仅用于完成传话服务。";
|
||||
101
src/config/reply.ts
Normal file
101
src/config/reply.ts
Normal file
@ -0,0 +1,101 @@
|
||||
export interface ReplyItem {
|
||||
id: string;
|
||||
contactPhone: string;
|
||||
originalMessage: string;
|
||||
replyMessage: string;
|
||||
replyTime: string;
|
||||
source: "sms" | "manual";
|
||||
unread: boolean;
|
||||
}
|
||||
|
||||
export const REPLY_PAGE_SIZE = 6;
|
||||
|
||||
export const REPLY_SOURCE_META: Record<ReplyItem["source"], { text: string; className: string }> = {
|
||||
sms: {
|
||||
text: "短信回复",
|
||||
className: "source-sms",
|
||||
},
|
||||
manual: {
|
||||
text: "人工传话",
|
||||
className: "source-manual",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_REPLY_LIST: ReplyItem[] = [
|
||||
{
|
||||
id: "RP250503001",
|
||||
contactPhone: "19878384602",
|
||||
originalMessage: "有些话想认真说一次,不打扰你,只希望你知道我的歉意。",
|
||||
replyMessage: "我收到了。谢谢你愿意认真说这些,我需要一点时间想想。",
|
||||
replyTime: "2026-05-03 20:12:00",
|
||||
source: "sms",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "RP250503002",
|
||||
contactPhone: "13993621665",
|
||||
originalMessage: "之前没有照顾好你的感受,对不起。愿你之后都能轻松一点。",
|
||||
replyMessage: "我已经看到了,也希望你之后能真的慢慢变好。",
|
||||
replyTime: "2026-05-03 18:35:00",
|
||||
source: "manual",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "RP250502003",
|
||||
contactPhone: "18826830915",
|
||||
originalMessage: "如果还有机会,我想把没说好的话认真补上。",
|
||||
replyMessage: "先不用急着见面,等彼此都平静一点再说吧。",
|
||||
replyTime: "2026-05-02 21:08:00",
|
||||
source: "sms",
|
||||
unread: false,
|
||||
},
|
||||
{
|
||||
id: "RP250501004",
|
||||
contactPhone: "15188340219",
|
||||
originalMessage: "这条短信只是想把心意说清楚,不给你压力。",
|
||||
replyMessage: "谢谢你的祝福,我也希望我们都能好好生活。",
|
||||
replyTime: "2026-05-01 09:46:00",
|
||||
source: "sms",
|
||||
unread: false,
|
||||
},
|
||||
{
|
||||
id: "RP250430005",
|
||||
contactPhone: "13677268104",
|
||||
originalMessage: "对不起,之前的争吵我也有很多没有处理好的地方。",
|
||||
replyMessage: "我知道了。过去的事情先放一放吧,别再互相消耗了。",
|
||||
replyTime: "2026-04-30 22:20:00",
|
||||
source: "manual",
|
||||
unread: false,
|
||||
},
|
||||
{
|
||||
id: "RP250429006",
|
||||
contactPhone: "19945201866",
|
||||
originalMessage: "想把晚安认真补给你,也想把歉意说清楚。",
|
||||
replyMessage: "晚安。也祝你以后能遇到更好的自己。",
|
||||
replyTime: "2026-04-29 23:17:00",
|
||||
source: "sms",
|
||||
unread: false,
|
||||
},
|
||||
{
|
||||
id: "RP250428007",
|
||||
contactPhone: "16690438152",
|
||||
originalMessage: "我不会再频繁打扰,只是想最后认真表达一次。",
|
||||
replyMessage: "收到。谢谢你尊重我的节奏。",
|
||||
replyTime: "2026-04-28 16:02:00",
|
||||
source: "manual",
|
||||
unread: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const serializeReply = (reply: ReplyItem): string => encodeURIComponent(JSON.stringify(reply));
|
||||
|
||||
export const deserializeReply = (payload?: string): ReplyItem | null => {
|
||||
if (!payload) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(payload)) as ReplyItem;
|
||||
} catch (error) {
|
||||
console.error("解析回复详情失败", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
32
src/config/routes.ts
Normal file
32
src/config/routes.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export const APP_ROUTES = {
|
||||
home: "/pages/index/index",
|
||||
mailbox: "/pages/mailbox/index",
|
||||
mine: "/pages/mine/index",
|
||||
sending: "/pages/sending/index",
|
||||
manual: "/pages/manual/index",
|
||||
calling: "/pages/calling/index",
|
||||
planning: "/pages/planning/index",
|
||||
planned: "/pages/planning/planned",
|
||||
reply: "/pages/reply/index",
|
||||
} as const;
|
||||
|
||||
export const TAB_BAR_ITEMS = [
|
||||
{
|
||||
pagePath: "pages/index/index",
|
||||
text: "写信",
|
||||
iconPath: "static/images/send.png",
|
||||
selectedIconPath: "static/images/send-active.png",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/mailbox/index",
|
||||
text: "信箱",
|
||||
iconPath: "static/images/mailbox.png",
|
||||
selectedIconPath: "static/images/mailbox-active.png",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/mine/index",
|
||||
text: "我的",
|
||||
iconPath: "static/images/mine.png",
|
||||
selectedIconPath: "static/images/mine-active.png",
|
||||
},
|
||||
] as const;
|
||||
@ -35,6 +35,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mailbox/detail",
|
||||
"style": {
|
||||
// #ifdef H5
|
||||
"titleNView": false,
|
||||
// #endif
|
||||
"navigationBarTitleText": "短信详情",
|
||||
"mp-alipay": {
|
||||
"titleBarColor": "#FFFFFF",
|
||||
"titleColor": "#000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
@ -113,6 +126,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/reply/detail",
|
||||
"style": {
|
||||
// #ifdef H5
|
||||
"titleNView": false,
|
||||
// #endif
|
||||
"navigationBarTitleText": "回复详情",
|
||||
"mp-alipay": {
|
||||
"titleBarColor": "#FFFFFF",
|
||||
"titleColor": "#000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/planning/planned",
|
||||
"style": {
|
||||
@ -134,10 +160,14 @@
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#999",
|
||||
"selectedColor": "#00c853",
|
||||
"color": "#7a7f87",
|
||||
"selectedColor": "#18b566",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "white",
|
||||
"borderStyle": "black",
|
||||
"height": "56px",
|
||||
"fontSize": "12px",
|
||||
"iconWidth": "24px",
|
||||
"spacing": "4px",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
|
||||
@ -1,334 +1,343 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<home-hero-card
|
||||
:eyebrow="HOME_HERO.eyebrow"
|
||||
:title="HOME_HERO.title"
|
||||
:subtitle="HOME_HERO.subtitle"
|
||||
:highlights="HOME_HERO.highlights"
|
||||
:image="HOME_HERO.image"
|
||||
:action-text="HOME_HERO.actionText"
|
||||
:secondary-text="HOME_HERO.secondaryText"
|
||||
@action="navigateTo(HOME_HERO.actionRoute)"
|
||||
@secondary="navigateTo(HOME_HERO.secondaryRoute)"
|
||||
/>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-title">
|
||||
<text class="title-text">很多遗憾&源于当初没有勇敢</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息卡片 -->
|
||||
<view class="message-card">
|
||||
<view class="message-content">
|
||||
<view class="message-types">
|
||||
<text class="message-type-text">分手挽回, 生日祝福、</text>
|
||||
<text class="message-type-text">道歉、告白、时空信也</text>
|
||||
<text class="message-type-text">可以发送哦</text>
|
||||
<view class="action-button">
|
||||
<button class="write-button" @tap="navigateToSending">开始写信</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="illustration">
|
||||
<!-- 注意:需要替换成实际的图片路径 -->
|
||||
<image class="illustration-image" src="../../assets/love.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-group">
|
||||
|
||||
<!-- 人工传话卡片 -->
|
||||
<view class="function-card">
|
||||
<view class="function-left">
|
||||
<view class="icon-wrapper icon-wrapper-orange">
|
||||
<image class="function-icon" src="../../assets/2.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="function-text">
|
||||
<text class="function-title">人工传话</text>
|
||||
<text class="function-description">成功率更高,更有仪式感</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="function-right">
|
||||
<!-- "去传话"按钮 -->
|
||||
<button class="function-button color-button" hover-class="button-hover" @tap="navigateToManual">
|
||||
<view class="button-content">
|
||||
<text>去传话</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 和解电话卡片【第一版本暂时不开发】 -->
|
||||
<view class="function-card">
|
||||
<view class="function-left">
|
||||
<view class="icon-wrapper icon-wrapper-blue">
|
||||
<image class="function-icon" src="../../assets/1.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="function-text">
|
||||
<text class="function-title">和解电话</text>
|
||||
<text class="function-description">被拉黑也能打通的电话</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="function-right">
|
||||
<!-- "去拨打"按钮 -->
|
||||
<button class="function-button color-button" hover-class="button-hover" @tap="navigateToCalling">
|
||||
<view class="button-content">
|
||||
<text>去拨打</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view class="section-block">
|
||||
<view class="section-header">
|
||||
<text class="section-title">选择表达方式</text>
|
||||
<text class="section-desc">按场景选择,先把话说清楚</text>
|
||||
</view>
|
||||
|
||||
<home-feature-card
|
||||
v-for="feature in HOME_PRIMARY_FEATURES"
|
||||
:key="feature.id"
|
||||
:title="feature.title"
|
||||
:description="feature.description"
|
||||
:icon-path="feature.iconPath"
|
||||
:button-text="feature.buttonText"
|
||||
:icon-class="feature.iconClass"
|
||||
:button-class="feature.buttonClass"
|
||||
:tag="feature.tag"
|
||||
@action="handleFeatureTap(feature)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!--居中消息提示组件,移到卡片外部-->
|
||||
|
||||
<tui-tips ref="tuiTips" position="center" backgroundColor="rgba(0, 0, 0, 0.8)" color="#fff" :size="30"></tui-tips>
|
||||
|
||||
<view class="card-group additional-cards">
|
||||
<!-- 使用v-for循环渲染动态卡片 -->
|
||||
<view class="function-card" v-for="(card, index) in additionalCards" :key="index">
|
||||
<view class="function-left">
|
||||
<view class="icon-wrapper" :class="getIconClass(index)">
|
||||
<image class="function-icon" :src="card.iconPath" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="function-text">
|
||||
<text class="function-title">{{ card.title }}</text>
|
||||
<text class="function-description">{{ card.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="function-right">
|
||||
<button class="function-button" :class="getButtonClass(index)" hover-class="button-hover" @tap="navigateToPlanning">
|
||||
<view class="button-content">
|
||||
<text>{{ card.buttonText }}</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view class="scenario-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">适合这些时刻</text>
|
||||
<text class="section-desc">告白、道歉、祝福都需要分寸感</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态显示 -->
|
||||
<view class="loading-container" v-if="isLoading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!--加载失败时显示-->
|
||||
<view class="error-container" v-if="loadError">
|
||||
<text class="error-text">{{ loadError }}</text>
|
||||
<button class="retry-button" @tap="fetchAdditionalCards">重试</button>
|
||||
<view class="scenario-list">
|
||||
<view class="scenario-card" v-for="scenario in HOME_SCENARIOS" :key="scenario.title">
|
||||
<view class="scenario-dot"></view>
|
||||
<view class="scenario-copy">
|
||||
<text class="scenario-title">{{ scenario.title }}</text>
|
||||
<text class="scenario-desc">{{ scenario.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="step-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">三步送达</text>
|
||||
<text class="section-desc">发送前还能再次预览确认</text>
|
||||
</view>
|
||||
|
||||
<view class="step-list">
|
||||
<view class="step-item" v-for="(step, index) in HOME_STEPS" :key="step">
|
||||
<text class="step-index">{{ index + 1 }}</text>
|
||||
<text class="step-text">{{ step }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-block additional-cards">
|
||||
<view class="section-header">
|
||||
<text class="section-title">更多工具</text>
|
||||
<text class="section-desc">让表达更准时、更稳妥</text>
|
||||
</view>
|
||||
|
||||
<home-feature-card
|
||||
v-for="(feature, index) in additionalCards"
|
||||
:key="feature.id"
|
||||
:title="feature.title"
|
||||
:description="feature.description"
|
||||
:icon-path="feature.iconPath"
|
||||
:button-text="feature.buttonText"
|
||||
:icon-class="getIconClass(index)"
|
||||
:button-class="getButtonClass(index)"
|
||||
:tag="feature.tag"
|
||||
@action="handleFeatureTap(feature)"
|
||||
/>
|
||||
|
||||
<inline-state v-if="isLoading" text="加载中..." type="loading" />
|
||||
<inline-state v-if="loadError" :text="loadError" type="error" retry-text="重试" @retry="fetchAdditionalCards" />
|
||||
</view>
|
||||
|
||||
<view class="guardrail-bar">
|
||||
<text class="guardrail-item" v-for="item in HOME_GUARDRAILS" :key="item">{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onPullDownRefresh } from "@dcloudio/uni-app";
|
||||
|
||||
import { onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import {useSwitchTab} from "@/stores/switchTab";
|
||||
import InlineState from "@/components/app/InlineState.vue";
|
||||
import HomeFeatureCard from "@/components/home/HomeFeatureCard.vue";
|
||||
import HomeHeroCard from "@/components/home/HomeHeroCard.vue";
|
||||
import TuiTips from "@/components/thorui/tui-tips/tui-tips.vue";
|
||||
import {
|
||||
HOME_ADDITIONAL_FEATURES,
|
||||
HOME_FEATURE_STYLE_CLASSES,
|
||||
HOME_GUARDRAILS,
|
||||
HOME_HERO,
|
||||
HOME_PRIMARY_FEATURES,
|
||||
HOME_SCENARIOS,
|
||||
HOME_STEPS,
|
||||
type HomeFeatureCardConfig,
|
||||
} from "@/config/home";
|
||||
import { useSwitchTab } from "@/stores/switchTab";
|
||||
|
||||
interface Card {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
iconPath: string;
|
||||
buttonText: string;
|
||||
pageUrl?: string; // 可选属性
|
||||
}
|
||||
|
||||
// 定义网站配置的接口类型
|
||||
interface IndexConfig {
|
||||
title: string;
|
||||
}
|
||||
|
||||
// 动态加载的卡片数据
|
||||
const additionalCards = ref<Card[]>([]);
|
||||
// 遮罩层状态
|
||||
const additionalCards = ref<HomeFeatureCardConfig[]>([]);
|
||||
const isLoading = ref<boolean>(false);
|
||||
const loadError = ref<string>('');
|
||||
|
||||
// tui-tips消息控制
|
||||
const loadError = ref<string>("");
|
||||
const tuiTips = ref<any>(null);
|
||||
|
||||
// 获取图标样式类
|
||||
const getIconClass = (index: number): string => {
|
||||
const classes = ['icon-wrapper-green', 'icon-wrapper-purple', 'icon-wrapper-red'];
|
||||
const classes = HOME_FEATURE_STYLE_CLASSES.icon;
|
||||
return classes[index % classes.length];
|
||||
};
|
||||
|
||||
// 获取按钮样式类
|
||||
const getButtonClass = (index: number): string => {
|
||||
const classes = ['green-button', 'purple-button', 'red-button'];
|
||||
const classes = HOME_FEATURE_STYLE_CLASSES.button;
|
||||
return classes[index % classes.length];
|
||||
};
|
||||
|
||||
// 显示提示消息
|
||||
const showTip = (message: string, duration: number = 2000): void => {
|
||||
if (tuiTips.value) {
|
||||
tuiTips.value.showTips({
|
||||
msg: message,
|
||||
duration: duration
|
||||
});
|
||||
const showTip = (message: string, duration = 2000): void => {
|
||||
tuiTips.value?.showTips({
|
||||
msg: message,
|
||||
duration,
|
||||
});
|
||||
};
|
||||
|
||||
const navigateTo = (route?: string): void => {
|
||||
if (!route) return;
|
||||
|
||||
uni.navigateTo({
|
||||
url: route,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFeatureTap = (feature: HomeFeatureCardConfig): void => {
|
||||
if (feature.disabledTip) {
|
||||
showTip(feature.disabledTip);
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo(feature.route);
|
||||
};
|
||||
|
||||
// 导航到通话页面
|
||||
const navigateToCalling = (): void => {
|
||||
console.log("触发跳转其他页面");
|
||||
|
||||
// 显示提示消息
|
||||
showTip('🚧 此通道正在维护中,暂时禁用!', 2000);
|
||||
|
||||
// 在控制台输出更多信息
|
||||
console.log("用户尝试访问正在开发中的功能: 和解电话");
|
||||
|
||||
// 暂时禁用导航
|
||||
// setTimeout(() => {
|
||||
// uni.navigateTo({
|
||||
// url: '/pages/calling/index',
|
||||
// });
|
||||
// }, 3000);
|
||||
};
|
||||
|
||||
// 导航到人工页面
|
||||
const navigateToManual = (): void => {
|
||||
console.log("触发跳转其他页面")
|
||||
uni.navigateTo({
|
||||
url: '/pages/manual/index',
|
||||
});
|
||||
};
|
||||
// 导航到消息页面
|
||||
const navigateToSending = (): void => {
|
||||
console.log("触发跳转其他页面")
|
||||
uni.navigateTo({
|
||||
url: '/pages/sending/index',
|
||||
});
|
||||
};
|
||||
// 导航到计划页面
|
||||
const navigateToPlanning = (): void => {
|
||||
console.log("触发跳转其他页面")
|
||||
uni.navigateTo({
|
||||
url: '/pages/planning/index',
|
||||
});
|
||||
};
|
||||
|
||||
// 导航到回复页面
|
||||
const navigateToReply = (): void => {
|
||||
console.log("触发跳转到回复页面");
|
||||
uni.navigateTo({
|
||||
url: '/pages/reply/index',
|
||||
});
|
||||
};
|
||||
|
||||
// 获取额外卡片的数据
|
||||
const fetchAdditionalCards = (): void => {
|
||||
isLoading.value = true;
|
||||
loadError.value = '';
|
||||
loadError.value = "";
|
||||
|
||||
// 模拟API延迟
|
||||
setTimeout(() => {
|
||||
// 使用模拟数据
|
||||
additionalCards.value = [
|
||||
{
|
||||
id: 1,
|
||||
title: '记忆时光机',
|
||||
description: '回忆美好时光,重温幸福',
|
||||
iconPath: '/static/images/cat.png',
|
||||
buttonText: '去回忆'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '情感咨询',
|
||||
description: '专业导师一对一指导',
|
||||
iconPath: '/static/images/cat.png',
|
||||
buttonText: '去咨询'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '关系修复',
|
||||
description: '重建信任,恢复亲密',
|
||||
iconPath: '/static/images/cat.png',
|
||||
buttonText: '去修复'
|
||||
}
|
||||
];
|
||||
additionalCards.value = HOME_ADDITIONAL_FEATURES;
|
||||
isLoading.value = false;
|
||||
}, 1000); // 模拟1秒的加载时间
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
// 初始化tab的值
|
||||
const initLastPage = (): void => {
|
||||
const switchTab = useSwitchTab();
|
||||
const currentPages = getCurrentPages();
|
||||
const lastPage = currentPages[currentPages.length - 1];
|
||||
const page = lastPage ? String(lastPage.route) : '';
|
||||
const page = lastPage ? String(lastPage.route) : "";
|
||||
switchTab.setLastPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
console.log("内容已载入")
|
||||
// 初始化页面tab的数据
|
||||
initLastPage();
|
||||
|
||||
// 加载首页数据
|
||||
fetchAdditionalCards();
|
||||
});
|
||||
|
||||
// 下拉刷新钩子
|
||||
onPullDownRefresh(() => {
|
||||
console.log("下拉刷新触发事件")
|
||||
fetchAdditionalCards();
|
||||
uni.stopPullDownRefresh();
|
||||
console.log("下拉刷新触发完成")
|
||||
})
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
@import '../../theme/index/index.css';
|
||||
|
||||
.custom-write-button {
|
||||
background: linear-gradient(135deg, #537edc, #84a9ff);
|
||||
border: none;
|
||||
padding: 0;
|
||||
border-radius: 30rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6rpx 12rpx rgba(83, 126, 220, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 200rpx;
|
||||
}
|
||||
|
||||
.custom-write-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #4568b2, #7090e7);
|
||||
border-radius: 30rpx;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.button-inner {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10rpx 24rpx;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 244, 250, 0.98) 0%, rgba(239, 248, 255, 0.94) 46%, #f8f8fb 100%),
|
||||
linear-gradient(120deg, rgba(255, 210, 228, 0.26), rgba(196, 231, 255, 0.22));
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 24rpx 24rpx 44rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
color: #fff;
|
||||
.section-block,
|
||||
.scenario-section,
|
||||
.step-section {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18rpx;
|
||||
padding: 0 6rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #20273a;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
color: #8b94a3;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.4;
|
||||
margin-left: 16rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.scenario-list {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(250, 253, 255, 0.82)),
|
||||
linear-gradient(90deg, rgba(255, 205, 223, 0.18), rgba(202, 235, 255, 0.18));
|
||||
border-radius: 26rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(83, 96, 130, 0.07);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.84);
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f0f2f5;
|
||||
}
|
||||
|
||||
.scenario-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.scenario-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ff7fa0, #7fc8ff);
|
||||
margin-top: 13rpx;
|
||||
margin-right: 18rpx;
|
||||
box-shadow: 0 0 0 8rpx rgba(255, 127, 160, 0.12);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.scenario-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
color: #253044;
|
||||
font-size: 29rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.scenario-desc {
|
||||
color: #7b8493;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1rpx;
|
||||
line-height: 1.5;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
margin-left: 6rpx;
|
||||
.step-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
flex: 1;
|
||||
min-height: 156rpx;
|
||||
margin-right: 16rpx;
|
||||
padding: 22rpx 18rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
box-shadow: 0 12rpx 30rpx rgba(83, 96, 130, 0.07);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.84);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.step-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.step-index {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: bounceRight 1.5s infinite;
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff82a2, #73bfff);
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
line-height: 38rpx;
|
||||
box-shadow: 0 8rpx 16rpx rgba(115, 191, 255, 0.18);
|
||||
}
|
||||
</style>
|
||||
|
||||
.step-text {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: #3a4354;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.additional-cards {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.guardrail-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 24rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.66);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 8rpx 22rpx rgba(83, 96, 130, 0.05);
|
||||
}
|
||||
|
||||
.guardrail-item {
|
||||
color: #7b8493;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.3;
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
475
src/pages/mailbox/detail.vue
Normal file
475
src/pages/mailbox/detail.vue
Normal file
@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<view class="dream-bg dream-bg-one"></view>
|
||||
<view class="dream-bg dream-bg-two"></view>
|
||||
|
||||
<view class="detail-header">
|
||||
<view class="back-button" hover-class="button-hover" @tap="goBack">‹</view>
|
||||
<view>
|
||||
<text class="detail-title">短信详情</text>
|
||||
<text class="detail-subtitle">查看这条匿名短信的发送进度</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="status-card" :class="statusMeta.className">
|
||||
<view class="status-top">
|
||||
<view class="status-pill" :class="statusMeta.className">
|
||||
<text class="status-dot"></text>
|
||||
<text>{{ statusMeta.text }}</text>
|
||||
</view>
|
||||
<text class="order-text">#{{ currentMail.orderNumber }}</text>
|
||||
</view>
|
||||
<text class="status-title">{{ statusTitle }}</text>
|
||||
<text class="status-desc">{{ statusDesc }}</text>
|
||||
</view>
|
||||
|
||||
<view class="phone-preview">
|
||||
<view class="preview-header">
|
||||
<view class="avatar">匿</view>
|
||||
<view>
|
||||
<text class="preview-name">匿名短信</text>
|
||||
<text class="preview-recipient">发给 {{ maskedRecipient }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="imessage-area">
|
||||
<view class="sms-bubble">
|
||||
<text class="sms-content">{{ currentMail.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-panel">
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系人</text>
|
||||
<text class="info-value">{{ maskedRecipient }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">提交时间</text>
|
||||
<text class="info-value">{{ submitTime }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">发送时间</text>
|
||||
<text class="info-value">{{ sendTime }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">费用</text>
|
||||
<text class="info-value">¥{{ currentMail.price.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="timeline-panel">
|
||||
<view class="timeline-item done">
|
||||
<view class="timeline-dot"></view>
|
||||
<view class="timeline-content">
|
||||
<text class="timeline-title">已提交</text>
|
||||
<text class="timeline-time">{{ submitTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="timeline-item" :class="{ done: currentMail.status === sentStatus, failed: currentMail.status === failedStatus }">
|
||||
<view class="timeline-dot"></view>
|
||||
<view class="timeline-content">
|
||||
<text class="timeline-title">{{ currentMail.status === failedStatus ? "发送失败" : "等待发送" }}</text>
|
||||
<text class="timeline-time">{{ sendTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
import {
|
||||
MAIL_STATUS_META,
|
||||
MailStatus,
|
||||
deserializeMail,
|
||||
formatMailTime,
|
||||
maskRecipient,
|
||||
type Mail,
|
||||
} from "@/config/mail";
|
||||
|
||||
const fallbackMail: Mail = {
|
||||
id: 0,
|
||||
orderNumber: "2503000000000000",
|
||||
recipient: "13800138000",
|
||||
content: "这是一条匿名短信记录,稍后会展示真实发送内容。",
|
||||
sendTime: "2025-03-18 20:00:00",
|
||||
submitTime: "2025-03-18 19:45:00",
|
||||
status: MailStatus.PENDING,
|
||||
price: 0,
|
||||
isNew: false,
|
||||
};
|
||||
|
||||
const mail = ref<Mail>(fallbackMail);
|
||||
const sentStatus = MailStatus.SENT;
|
||||
const failedStatus = MailStatus.FAILED;
|
||||
|
||||
onLoad((query) => {
|
||||
const detail = deserializeMail(query?.mail as string | undefined);
|
||||
|
||||
if (detail) {
|
||||
mail.value = detail;
|
||||
}
|
||||
});
|
||||
|
||||
const currentMail = computed(() => mail.value);
|
||||
const statusMeta = computed(() => MAIL_STATUS_META[currentMail.value.status]);
|
||||
const maskedRecipient = computed(() => maskRecipient(currentMail.value.recipient));
|
||||
const submitTime = computed(() => formatMailTime(currentMail.value.submitTime));
|
||||
const sendTime = computed(() => formatMailTime(currentMail.value.sendTime));
|
||||
|
||||
const statusTitle = computed(() => {
|
||||
if (currentMail.value.status === MailStatus.SENT) return "短信已经送达";
|
||||
if (currentMail.value.status === MailStatus.FAILED) return "发送没有成功";
|
||||
return "短信正在等待发送";
|
||||
});
|
||||
|
||||
const statusDesc = computed(() => {
|
||||
if (currentMail.value.status === MailStatus.SENT) return "对方收到后,如果有回复会在信箱中展示。";
|
||||
if (currentMail.value.status === MailStatus.FAILED) return "可以稍后重新提交,或检查联系人号码是否正确。";
|
||||
return "系统会按预约时间发送,请留意后续状态变化。";
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.switchTab({
|
||||
url: "/pages/mailbox/index",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx 26rpx 48rpx;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 244, 250, 0.98) 0%, rgba(238, 248, 255, 0.96) 48%, #f7f8fb 100%),
|
||||
linear-gradient(135deg, rgba(255, 204, 225, 0.22), rgba(199, 233, 255, 0.24));
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dream-bg {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.dream-bg-one {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
right: -160rpx;
|
||||
top: 110rpx;
|
||||
background: rgba(255, 188, 214, 0.24);
|
||||
}
|
||||
|
||||
.dream-bg-two {
|
||||
width: 320rpx;
|
||||
height: 320rpx;
|
||||
left: -140rpx;
|
||||
top: 620rpx;
|
||||
background: rgba(167, 218, 255, 0.22);
|
||||
}
|
||||
|
||||
.detail-header,
|
||||
.status-card,
|
||||
.phone-preview,
|
||||
.info-panel,
|
||||
.timeline-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
line-height: 62rpx;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
color: #293247;
|
||||
font-size: 54rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 10rpx 24rpx rgba(83, 96, 130, 0.07);
|
||||
}
|
||||
|
||||
.button-hover {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.detail-title,
|
||||
.detail-subtitle,
|
||||
.status-title,
|
||||
.status-desc,
|
||||
.preview-name,
|
||||
.preview-recipient,
|
||||
.sms-content,
|
||||
.info-label,
|
||||
.info-value,
|
||||
.timeline-title,
|
||||
.timeline-time {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
color: #20273a;
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.detail-subtitle {
|
||||
margin-top: 8rpx;
|
||||
color: #687286;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
overflow: hidden;
|
||||
padding: 26rpx;
|
||||
border-radius: 28rpx;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.9) 0%, rgba(255, 248, 252, 0.82) 55%, rgba(240, 249, 255, 0.86) 100%),
|
||||
rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.88);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.09);
|
||||
}
|
||||
|
||||
.status-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 26rpx;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-pill.status-pending {
|
||||
background: linear-gradient(135deg, #ffb75e 0%, #f59e0b 100%);
|
||||
}
|
||||
|
||||
.status-pill.status-sent {
|
||||
background: linear-gradient(135deg, #7ee0a3 0%, #34b878 100%);
|
||||
}
|
||||
|
||||
.status-pill.status-failed {
|
||||
background: linear-gradient(135deg, #ff8daf 0%, #e85b7f 100%);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
margin-right: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.order-text {
|
||||
flex: 1;
|
||||
color: #9aa3b2;
|
||||
font-size: 22rpx;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
color: #263044;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
margin-top: 12rpx;
|
||||
color: #687286;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.phone-preview,
|
||||
.info-panel,
|
||||
.timeline-panel {
|
||||
margin-top: 22rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.08);
|
||||
}
|
||||
|
||||
.phone-preview {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 26rpx 28rpx 18rpx;
|
||||
border-bottom: 1rpx solid rgba(230, 236, 246, 0.78);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 74rpx;
|
||||
height: 74rpx;
|
||||
line-height: 74rpx;
|
||||
margin-right: 16rpx;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
|
||||
}
|
||||
|
||||
.preview-name {
|
||||
color: #263044;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preview-recipient {
|
||||
margin-top: 6rpx;
|
||||
color: #8c96a8;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.imessage-area {
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(180deg, rgba(247, 250, 255, 0.76), rgba(255, 248, 252, 0.72));
|
||||
}
|
||||
|
||||
.sms-bubble {
|
||||
max-width: 590rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 28rpx 28rpx 28rpx 8rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid rgba(232, 238, 248, 0.88);
|
||||
box-shadow: 0 10rpx 24rpx rgba(93, 107, 139, 0.06);
|
||||
}
|
||||
|
||||
.sms-content {
|
||||
color: #333b4f;
|
||||
font-size: 29rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
padding: 8rpx 26rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
min-height: 82rpx;
|
||||
border-bottom: 1rpx solid rgba(230, 236, 246, 0.72);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #8c96a8;
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #263044;
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.timeline-panel {
|
||||
padding: 26rpx 28rpx;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-item::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 11rpx;
|
||||
top: 28rpx;
|
||||
bottom: 0;
|
||||
width: 2rpx;
|
||||
background: rgba(200, 211, 226, 0.7);
|
||||
}
|
||||
|
||||
.timeline-item:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
margin-top: 4rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #d5dde9;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-item.done .timeline-dot {
|
||||
background: linear-gradient(135deg, #7ee0a3, #34b878);
|
||||
}
|
||||
|
||||
.timeline-item.failed .timeline-dot {
|
||||
background: linear-gradient(135deg, #ff8daf, #e85b7f);
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
color: #263044;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
margin-top: 8rpx;
|
||||
color: #8c96a8;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,248 +1,322 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- Header Section: Profile Picture and Name -->
|
||||
<view class="content-wrapper">
|
||||
<view class="header">
|
||||
<view class="profile-info">
|
||||
<view class="profile-pic-container">
|
||||
<image src="@/static/images/cat.png" mode="aspectFill" class="profile-pic"></image>
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<text class="username">GT-呆河马</text>
|
||||
<view class="id-copy-wrapper">
|
||||
<text class="user-id">ID: 10086</text>
|
||||
<text class="copy-id">复制ID</text>
|
||||
</view>
|
||||
<view class="dream-bg dream-bg-one"></view>
|
||||
<view class="dream-bg dream-bg-two"></view>
|
||||
|
||||
<scroll-view class="content-wrapper" scroll-y>
|
||||
<profile-header
|
||||
:avatar="PROFILE_INFO.avatar"
|
||||
:username="PROFILE_INFO.username"
|
||||
:user-id="PROFILE_INFO.userId"
|
||||
@copy-id="copyUserId"
|
||||
/>
|
||||
|
||||
<view class="stats-card">
|
||||
<view
|
||||
v-for="item in PROFILE_STATS"
|
||||
:key="item.label"
|
||||
class="stat-item"
|
||||
hover-class="button-hover"
|
||||
@tap="navigateTo(item.route)"
|
||||
>
|
||||
<text class="stat-value">{{ item.value }}</text>
|
||||
<text class="stat-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-block">
|
||||
<view class="section-header">
|
||||
<text class="section-title">常用操作</text>
|
||||
<text class="section-subtitle">更快开始一次表达</text>
|
||||
</view>
|
||||
<view class="quick-grid">
|
||||
<view
|
||||
v-for="item in PROFILE_QUICK_ACTIONS"
|
||||
:key="item.title"
|
||||
class="quick-card"
|
||||
hover-class="card-hover"
|
||||
@tap="navigateTo(item.route)"
|
||||
>
|
||||
<view class="quick-icon">{{ item.icon }}</view>
|
||||
<text class="quick-title">{{ item.title }}</text>
|
||||
<text class="quick-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Second Section: Interactions -->
|
||||
<view class="menu-section">
|
||||
<!-- <view class="menu-item" hover-class="menu-item-hover" @tap="navigateToMailbox">-->
|
||||
<!-- <text class="item-text">已发短信</text>-->
|
||||
<!-- <text class="arrow">›</text>-->
|
||||
<!-- </view>-->
|
||||
<view class="menu-item" hover-class="menu-item-hover" @tap="navigateToReply">
|
||||
<text class="item-text highlight">收到回复</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" hover-class="menu-item-hover" @tap="navigateToPlanned">
|
||||
<text class="item-text">已建计划</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<profile-menu-section
|
||||
v-for="(menuGroup, index) in profileMenuGroups"
|
||||
:key="index"
|
||||
:items="menuGroup"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- Additional Section -->
|
||||
<view class="menu-section">
|
||||
<!-- <view class="menu-item" hover-class="menu-item-hover">
|
||||
<text class="item-text highlight">通话记录</text>
|
||||
<text class="arrow">›</text>
|
||||
</view> -->
|
||||
<view class="menu-item" hover-class="menu-item-hover" @tap="copyWechatContact">
|
||||
<text class="item-text">联系我们</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Logout Button Section -->
|
||||
<view class="button-container">
|
||||
<tui-button btnSize="medium" shape="circle" size="28">退出登陆</tui-button>
|
||||
<button class="logout-button" hover-class="button-hover">退出登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Script logic here
|
||||
import TuiButton from "@/components/thorui/tui-button/tui-button.vue";
|
||||
import ProfileHeader from "@/components/profile/ProfileHeader.vue";
|
||||
import ProfileMenuSection, { type ProfileMenuItem } from "@/components/profile/ProfileMenuSection.vue";
|
||||
import { APP_ROUTES } from "@/config/routes";
|
||||
import {
|
||||
CONTACT_CONFIG,
|
||||
PROFILE_INFO,
|
||||
PROFILE_MENU_GROUPS,
|
||||
PROFILE_NOTICE_TEXT,
|
||||
PROFILE_PRIVACY_TEXT,
|
||||
PROFILE_QUICK_ACTIONS,
|
||||
PROFILE_STATS,
|
||||
} from "@/config/profile";
|
||||
|
||||
const profileMenuGroups = PROFILE_MENU_GROUPS.map((group) => [...group]) as ProfileMenuItem[][];
|
||||
|
||||
const navigateTo = (route?: string): void => {
|
||||
if (!route) return;
|
||||
|
||||
if (route === APP_ROUTES.home || route === APP_ROUTES.mailbox || route === APP_ROUTES.mine) {
|
||||
uni.switchTab({
|
||||
url: route,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 导航到消息页面
|
||||
const navigateToMailbox = (): void => {
|
||||
console.log("触发跳转其他页面")
|
||||
uni.navigateTo({
|
||||
url: '/pages/mailbox/index',
|
||||
url: route,
|
||||
});
|
||||
};
|
||||
|
||||
// 导航到计划页面
|
||||
const navigateToReply = (): void => {
|
||||
console.log("触发跳转其他页面")
|
||||
uni.navigateTo({
|
||||
url: '/pages/reply/index',
|
||||
});
|
||||
const handleMenuSelect = (item: ProfileMenuItem): void => {
|
||||
if (item.action === "copyWechat") {
|
||||
copyWechatContact();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.action === "privacy") {
|
||||
showInfo(PROFILE_PRIVACY_TEXT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.action === "notice") {
|
||||
showInfo(PROFILE_NOTICE_TEXT);
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo(item.route);
|
||||
};
|
||||
|
||||
// 导航到计划页面
|
||||
const navigateToPlanned = (): void => {
|
||||
console.log("触发跳转其他页面")
|
||||
uni.navigateTo({
|
||||
url: '/pages/planning/planned',
|
||||
});
|
||||
};
|
||||
|
||||
// 复制微信联系方式
|
||||
const copyWechatContact = (): void => {
|
||||
const wechatID = "I18888"; // 要复制的微信号
|
||||
|
||||
const copyUserId = (): void => {
|
||||
uni.setClipboardData({
|
||||
data: wechatID,
|
||||
data: PROFILE_INFO.userId,
|
||||
success: () => {
|
||||
// 显示提示信息
|
||||
uni.showToast({
|
||||
title: '微信号已复制',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
title: "ID已复制",
|
||||
icon: "success",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const copyWechatContact = (): void => {
|
||||
uni.setClipboardData({
|
||||
data: CONTACT_CONFIG.wechatId,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: CONTACT_CONFIG.copySuccessText,
|
||||
icon: "success",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '复制失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
title: CONTACT_CONFIG.copyFailText,
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showInfo = (content: string): void => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content,
|
||||
showCancel: false,
|
||||
confirmText: "知道了",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* Global page styles to prevent scrolling */
|
||||
page {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
background-color: #f5f7fa;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 244, 250, 0.98) 0%, rgba(239, 248, 255, 0.96) 48%, #f7f8fb 100%),
|
||||
linear-gradient(135deg, rgba(255, 204, 225, 0.22), rgba(199, 233, 255, 0.24));
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
.dream-bg {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
.dream-bg-one {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
right: -150rpx;
|
||||
top: 90rpx;
|
||||
background: rgba(255, 193, 215, 0.23);
|
||||
}
|
||||
|
||||
.dream-bg-two {
|
||||
width: 330rpx;
|
||||
height: 330rpx;
|
||||
left: -140rpx;
|
||||
top: 620rpx;
|
||||
background: rgba(171, 218, 255, 0.21);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
.stats-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 22rpx;
|
||||
padding: 20rpx 10rpx;
|
||||
border-radius: 26rpx;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.84), rgba(247, 252, 255, 0.62)),
|
||||
rgba(255, 255, 255, 0.7);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 16rpx 36rpx rgba(93, 107, 139, 0.07);
|
||||
}
|
||||
|
||||
.profile-pic-container {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid rgba(230, 236, 246, 0.82);
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid #fff;
|
||||
.stat-item:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.id-copy-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.copy-id {
|
||||
color: #4285f4;
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
padding: 2px 6px;
|
||||
background-color: rgba(66, 133, 244, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
.stat-value,
|
||||
.stat-label,
|
||||
.section-title,
|
||||
.section-subtitle,
|
||||
.quick-title,
|
||||
.quick-desc {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #20273a;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 8rpx;
|
||||
color: #8c96a8;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.section-block {
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
padding: 0 4rpx 14rpx;
|
||||
}
|
||||
|
||||
.menu-item-hover {
|
||||
background-color: #f9f9f9;
|
||||
.section-title {
|
||||
color: #20273a;
|
||||
font-size: 31rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
.section-subtitle {
|
||||
color: #8c96a8;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #4CAF50;
|
||||
font-weight: 500;
|
||||
.quick-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #ccc;
|
||||
font-size: 22px;
|
||||
font-weight: 300;
|
||||
.quick-card {
|
||||
width: calc(50% - 8rpx);
|
||||
min-height: 150rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 22rpx;
|
||||
border-radius: 24rpx;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.82), rgba(255, 248, 252, 0.62)),
|
||||
rgba(255, 255, 255, 0.64);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.88);
|
||||
box-shadow: 0 14rpx 32rpx rgba(93, 107, 139, 0.07);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
line-height: 56rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 18rpx;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
|
||||
}
|
||||
|
||||
.quick-title {
|
||||
color: #263044;
|
||||
font-size: 27rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.quick-desc {
|
||||
margin-top: 8rpx;
|
||||
color: #8c96a8;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
@ -250,30 +324,27 @@ page {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
margin-top: auto;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
padding: 18rpx 0 40rpx;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
color: #ff3b30;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding: 16px 0;
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
line-height: 1;
|
||||
height: 86rpx;
|
||||
line-height: 86rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #d94f75;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 14rpx 30rpx rgba(83, 96, 130, 0.07);
|
||||
}
|
||||
|
||||
.logout-button-hover {
|
||||
background-color: #fff0f0;
|
||||
.logout-button::after {
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
.button-hover {
|
||||
opacity: 0.76;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,446 +1,324 @@
|
||||
<script setup lang="ts">
|
||||
import TuiTabs from "@/components/thorui/tui-tabs/tui-tabs.vue";
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
// 定义标签页
|
||||
const tabs = ref([
|
||||
{ name: "全部" },
|
||||
{ name: "进行中" },
|
||||
{ name: "已完成" }
|
||||
]);
|
||||
import PlanCard from "@/components/planning/PlanCard.vue";
|
||||
import { MOCK_PLAN_LIST, PLAN_STATUS_TABS, type PlanItem } from "@/config/planning";
|
||||
|
||||
// 当前选中的标签页
|
||||
const currentTab = ref(0);
|
||||
|
||||
// 定义计划数据的接口
|
||||
interface PlanData {
|
||||
id: string;
|
||||
phoneNumber: string;
|
||||
planType: string;
|
||||
planCycle: string;
|
||||
startTime: string;
|
||||
status: 'inProgress' | 'completed'; // 计划状态:进行中或已完成
|
||||
}
|
||||
|
||||
// 模拟全部计划数据 (作为数据源)
|
||||
const mockAllPlanData: PlanData[] = [
|
||||
{
|
||||
id: 'P001',
|
||||
phoneNumber: '1386****123',
|
||||
planType: '健身计划',
|
||||
planCycle: '7天',
|
||||
startTime: '2023-06-01',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P002',
|
||||
phoneNumber: '1590****456',
|
||||
planType: '学习计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-05-15',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P003',
|
||||
phoneNumber: '1377****789',
|
||||
planType: '阅读计划',
|
||||
planCycle: '半个月',
|
||||
startTime: '2023-05-20',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'P004',
|
||||
phoneNumber: '1865****234',
|
||||
planType: '工作计划',
|
||||
planCycle: '1天',
|
||||
startTime: '2023-06-05',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P005',
|
||||
phoneNumber: '1398****567',
|
||||
planType: '旅行计划',
|
||||
planCycle: '7天',
|
||||
startTime: '2023-04-10',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'P006',
|
||||
phoneNumber: '1592****890',
|
||||
planType: '饮食计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-05-01',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'P007',
|
||||
phoneNumber: '1356****345',
|
||||
planType: '写作计划',
|
||||
planCycle: '半个月',
|
||||
startTime: '2023-06-10',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P008',
|
||||
phoneNumber: '1867****678',
|
||||
planType: '省钱计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-05-25',
|
||||
status: 'inProgress'
|
||||
},
|
||||
// 额外添加更多数据以演示分页功能
|
||||
{
|
||||
id: 'P009',
|
||||
phoneNumber: '1377****111',
|
||||
planType: '学英语计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-06-15',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P010',
|
||||
phoneNumber: '1399****222',
|
||||
planType: '早起计划',
|
||||
planCycle: '7天',
|
||||
startTime: '2023-06-20',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P011',
|
||||
phoneNumber: '1566****333',
|
||||
planType: '跑步计划',
|
||||
planCycle: '半个月',
|
||||
startTime: '2023-05-30',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P012',
|
||||
phoneNumber: '1866****444',
|
||||
planType: '节食计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-05-01',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'P013',
|
||||
phoneNumber: '1388****555',
|
||||
planType: '学习编程计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-04-15',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'P014',
|
||||
phoneNumber: '1599****666',
|
||||
planType: '看电影计划',
|
||||
planCycle: '7天',
|
||||
startTime: '2023-06-25',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P015',
|
||||
phoneNumber: '1366****777',
|
||||
planType: '睡眠计划',
|
||||
planCycle: '半个月',
|
||||
startTime: '2023-06-01',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P016',
|
||||
phoneNumber: '1877****888',
|
||||
planType: '减肥计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-05-10',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'P017',
|
||||
phoneNumber: '1399****999',
|
||||
planType: '存钱计划',
|
||||
planCycle: '1个月',
|
||||
startTime: '2023-06-01',
|
||||
status: 'inProgress'
|
||||
},
|
||||
{
|
||||
id: 'P018',
|
||||
phoneNumber: '1590****000',
|
||||
planType: '阅读计划',
|
||||
planCycle: '半个月',
|
||||
startTime: '2023-05-15',
|
||||
status: 'completed'
|
||||
}
|
||||
];
|
||||
|
||||
// 分页控制变量
|
||||
const pageSize = 10; // 每页显示10条数据
|
||||
const currentPage = ref(1);
|
||||
const planList = ref<PlanItem[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 4;
|
||||
|
||||
// 展示的计划数据
|
||||
const allPlanData = ref<PlanData[]>([]);
|
||||
const currentTabValue = computed(() => PLAN_STATUS_TABS[currentTab.value].value);
|
||||
|
||||
const filteredSource = computed(() => {
|
||||
if (currentTabValue.value === "all") return MOCK_PLAN_LIST;
|
||||
return MOCK_PLAN_LIST.filter((item) => item.status === currentTabValue.value);
|
||||
});
|
||||
|
||||
const loadData = (page = 1, reset = false) => {
|
||||
if (isLoading.value) return;
|
||||
|
||||
// 加载数据方法
|
||||
const loadData = (page: number, tabIndex: number, refresh: boolean = false) => {
|
||||
isLoading.value = true;
|
||||
|
||||
// 根据标签页筛选源数据
|
||||
let filteredSource;
|
||||
if (tabIndex === 0) {
|
||||
// 全部
|
||||
filteredSource = mockAllPlanData;
|
||||
} else if (tabIndex === 1) {
|
||||
// 进行中
|
||||
filteredSource = mockAllPlanData.filter(item => item.status === 'inProgress');
|
||||
} else {
|
||||
// 已完成
|
||||
filteredSource = mockAllPlanData.filter(item => item.status === 'completed');
|
||||
}
|
||||
|
||||
// 模拟网络请求延迟
|
||||
|
||||
setTimeout(() => {
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const newData = filteredSource.slice(start, end);
|
||||
|
||||
// 如果是刷新,替换数据;否则追加数据
|
||||
if (refresh) {
|
||||
allPlanData.value = newData;
|
||||
} else {
|
||||
allPlanData.value = [...allPlanData.value, ...newData];
|
||||
}
|
||||
|
||||
// 更新分页状态
|
||||
const nextList = filteredSource.value.slice(start, end);
|
||||
|
||||
planList.value = reset ? nextList : [...planList.value, ...nextList];
|
||||
currentPage.value = page;
|
||||
hasMore.value = end < filteredSource.length;
|
||||
hasMore.value = end < filteredSource.value.length;
|
||||
isLoading.value = false;
|
||||
}, 500);
|
||||
}, 400);
|
||||
};
|
||||
|
||||
// 切换标签页时处理
|
||||
const changeTab = (e: { index: number }) => {
|
||||
currentTab.value = e.index;
|
||||
// 切换标签页时重置分页并重新加载数据
|
||||
const changeTab = (index: number) => {
|
||||
currentTab.value = index;
|
||||
currentPage.value = 1;
|
||||
loadData(1, e.index, true);
|
||||
hasMore.value = true;
|
||||
loadData(1, true);
|
||||
};
|
||||
|
||||
// 加载更多数据
|
||||
const onLoadMore = () => {
|
||||
if (!isLoading.value && hasMore.value) {
|
||||
loadData(currentPage.value + 1, currentTab.value);
|
||||
loadData(currentPage.value + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据标签页过滤数据
|
||||
const filteredPlanData = computed(() => {
|
||||
return allPlanData.value;
|
||||
});
|
||||
|
||||
// 页面加载时初始化数据
|
||||
onMounted(() => {
|
||||
// 初始加载第一页数据
|
||||
loadData(1, currentTab.value, true);
|
||||
loadData(1, true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 标签页 -->
|
||||
<tui-tabs :tabs="tabs" :isFixed="false" :currentTab="currentTab" itemWidth="33.3%" @change="changeTab"></tui-tabs>
|
||||
|
||||
<!-- 计划列表 -->
|
||||
<scroll-view
|
||||
class="plan-list-scroll"
|
||||
scroll-y
|
||||
@scrolltolower="onLoadMore"
|
||||
v-if="filteredPlanData.length > 0 || isLoading"
|
||||
>
|
||||
<view class="plan-list">
|
||||
<view
|
||||
v-for="(item, index) in filteredPlanData"
|
||||
:key="index"
|
||||
class="plan-card"
|
||||
:class="{'plan-card-in-progress': item.status === 'inProgress', 'plan-card-completed': item.status === 'completed'}"
|
||||
>
|
||||
<view class="plan-header">
|
||||
<text class="plan-type">{{ item.planType }}</text>
|
||||
<text class="plan-id">ID: {{ item.id }}</text>
|
||||
</view>
|
||||
|
||||
<view class="plan-divider"></view>
|
||||
|
||||
<view class="plan-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">手机号:</text>
|
||||
<text class="info-value">{{ item.phoneNumber }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">计划周期:</text>
|
||||
<text class="info-value">{{ item.planCycle }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">开始时间:</text>
|
||||
<text class="info-value">{{ item.startTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="planned-page">
|
||||
<view class="dream-bg dream-bg-one"></view>
|
||||
<view class="dream-bg dream-bg-two"></view>
|
||||
|
||||
<view class="page-header">
|
||||
<view>
|
||||
<text class="page-title">计划列表</text>
|
||||
<text class="page-subtitle">查看正在执行的周期传话计划</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="plan-tabs">
|
||||
<view
|
||||
v-for="(tab, index) in PLAN_STATUS_TABS"
|
||||
:key="tab.value"
|
||||
class="plan-tab"
|
||||
:class="{ active: currentTab === index }"
|
||||
@tap="changeTab(index)"
|
||||
>
|
||||
<text>{{ tab.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="plan-scroll" scroll-y @scrolltolower="onLoadMore">
|
||||
<view class="plan-list" v-if="planList.length > 0">
|
||||
<plan-card v-for="plan in planList" :key="plan.id" :plan="plan" />
|
||||
|
||||
<view v-if="isLoading" class="loading-state">
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态提示 -->
|
||||
<view class="loading-more" v-if="isLoading">
|
||||
<view class="loading-icon"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="no-more" v-if="!hasMore && !isLoading && filteredPlanData.length > 0">
|
||||
<text class="no-more-text">--- 已经到底了 ---</text>
|
||||
|
||||
<view v-if="!hasMore && !isLoading" class="no-more">
|
||||
<view class="no-more-line"></view>
|
||||
<text class="no-more-text">已经到底了</text>
|
||||
<view class="no-more-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" v-else-if="!isLoading">
|
||||
<view class="empty-card">
|
||||
<view class="empty-icon">计</view>
|
||||
<text class="empty-title">暂无计划</text>
|
||||
<text class="empty-desc">创建后会在这里看到计划执行状态</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="loading-state first-loading">
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
<view class="loading-dot"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 空数据状态 -->
|
||||
<view class="empty-state" v-if="filteredPlanData.length === 0 && !isLoading">
|
||||
<text class="empty-text">暂无相关计划数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
<style scoped lang="scss">
|
||||
.planned-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 244, 250, 0.98) 0%, rgba(239, 248, 255, 0.96) 48%, #f7f8fb 100%),
|
||||
linear-gradient(135deg, rgba(255, 204, 225, 0.22), rgba(199, 233, 255, 0.24));
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plan-list-scroll {
|
||||
.dream-bg {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.dream-bg-one {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
right: -150rpx;
|
||||
top: 110rpx;
|
||||
background: rgba(255, 193, 215, 0.23);
|
||||
}
|
||||
|
||||
.dream-bg-two {
|
||||
width: 330rpx;
|
||||
height: 330rpx;
|
||||
left: -140rpx;
|
||||
top: 650rpx;
|
||||
background: rgba(171, 218, 255, 0.21);
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.plan-tabs,
|
||||
.plan-scroll {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 28rpx 18rpx;
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-subtitle,
|
||||
.empty-title,
|
||||
.empty-desc {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #20273a;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin-top: 10rpx;
|
||||
color: #687286;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.plan-tabs {
|
||||
display: flex;
|
||||
margin: 0 24rpx 18rpx;
|
||||
padding: 8rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.86);
|
||||
box-shadow: 0 10rpx 26rpx rgba(83, 96, 130, 0.05);
|
||||
}
|
||||
|
||||
.plan-tab {
|
||||
flex: 1;
|
||||
height: calc(100vh - 100rpx);
|
||||
height: 62rpx;
|
||||
line-height: 62rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
color: #7b8493;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.plan-tab.active {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
|
||||
box-shadow: 0 10rpx 20rpx rgba(255, 120, 162, 0.18);
|
||||
}
|
||||
|
||||
.plan-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.plan-list {
|
||||
padding: 0 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.plan-card-in-progress {
|
||||
background-color: #e6f7ff; /* 浅蓝色背景表示进行中 */
|
||||
}
|
||||
|
||||
.plan-card-completed {
|
||||
background-color: #f6ffed; /* 浅绿色背景表示已完成 */
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.plan-type {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.plan-id {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.plan-divider {
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
margin: 12rpx 0 16rpx;
|
||||
}
|
||||
|
||||
.plan-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 加载更多样式 */
|
||||
.loading-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30rpx 0;
|
||||
padding: 38rpx 0;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 3rpx solid #f0f0f0;
|
||||
border-top: 3rpx solid #07c160;
|
||||
.first-loading {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
margin: 0 7rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 10rpx;
|
||||
animation: spin 1s linear infinite;
|
||||
background: linear-gradient(135deg, #ff92b6, #8bcfff);
|
||||
animation: loadingDot 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
.loading-dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
.loading-dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@keyframes loadingDot {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 0 34rpx;
|
||||
}
|
||||
|
||||
.no-more-line {
|
||||
width: 120rpx;
|
||||
height: 1rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(153, 170, 190, 0.35));
|
||||
}
|
||||
|
||||
.no-more-line:last-child {
|
||||
background: linear-gradient(90deg, rgba(153, 170, 190, 0.35), transparent);
|
||||
}
|
||||
|
||||
.no-more-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin: 0 18rpx;
|
||||
color: #9aa3b2;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 70rpx 24rpx;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 68rpx 34rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.08);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
.empty-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
.empty-title {
|
||||
color: #263044;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
margin-top: 10rpx;
|
||||
color: #8c96a8;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
306
src/pages/reply/detail.vue
Normal file
306
src/pages/reply/detail.vue
Normal file
@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<view class="dream-bg dream-bg-one"></view>
|
||||
<view class="dream-bg dream-bg-two"></view>
|
||||
|
||||
<view class="detail-header">
|
||||
<view class="back-button" hover-class="button-hover" @tap="goBack">‹</view>
|
||||
<view>
|
||||
<text class="detail-title">回复详情</text>
|
||||
<text class="detail-subtitle">{{ maskedContact }} · {{ replyTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="chat-panel">
|
||||
<view class="chat-time">{{ replyTime }}</view>
|
||||
|
||||
<view class="message-row sent-row">
|
||||
<view class="message-meta">
|
||||
<text>发送内容</text>
|
||||
</view>
|
||||
<view class="chat-bubble sent-bubble">
|
||||
<text>{{ currentReply.originalMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="message-row reply-row">
|
||||
<view class="reply-avatar">回</view>
|
||||
<view>
|
||||
<view class="message-meta reply-meta">
|
||||
<text>回复内容</text>
|
||||
</view>
|
||||
<view class="chat-bubble reply-bubble">
|
||||
<text>{{ currentReply.replyMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-panel">
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系人</text>
|
||||
<text class="info-value">{{ maskedContact }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">回复时间</text>
|
||||
<text class="info-value">{{ replyTime }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">记录编号</text>
|
||||
<text class="info-value">#{{ currentReply.id }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
import { formatMailTime, maskRecipient } from "@/config/mail";
|
||||
import { deserializeReply, type ReplyItem } from "@/config/reply";
|
||||
|
||||
const fallbackReply: ReplyItem = {
|
||||
id: "RP000000000",
|
||||
contactPhone: "13800138000",
|
||||
originalMessage: "这是一条原短信内容。",
|
||||
replyMessage: "这是一条回复内容。",
|
||||
replyTime: "2026-05-03 20:12:00",
|
||||
source: "sms",
|
||||
unread: false,
|
||||
};
|
||||
|
||||
const reply = ref<ReplyItem>(fallbackReply);
|
||||
|
||||
onLoad((query) => {
|
||||
const detail = deserializeReply(query?.reply as string | undefined);
|
||||
|
||||
if (detail) {
|
||||
reply.value = detail;
|
||||
}
|
||||
});
|
||||
|
||||
const currentReply = computed(() => reply.value);
|
||||
const maskedContact = computed(() => maskRecipient(currentReply.value.contactPhone));
|
||||
const replyTime = computed(() => formatMailTime(currentReply.value.replyTime));
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/reply/index",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx 26rpx 48rpx;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 244, 250, 0.98) 0%, rgba(239, 248, 255, 0.96) 48%, #f7f8fb 100%),
|
||||
linear-gradient(135deg, rgba(255, 204, 225, 0.22), rgba(199, 233, 255, 0.24));
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dream-bg {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.dream-bg-one {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
right: -160rpx;
|
||||
top: 110rpx;
|
||||
background: rgba(255, 188, 214, 0.24);
|
||||
}
|
||||
|
||||
.dream-bg-two {
|
||||
width: 320rpx;
|
||||
height: 320rpx;
|
||||
left: -140rpx;
|
||||
top: 620rpx;
|
||||
background: rgba(167, 218, 255, 0.22);
|
||||
}
|
||||
|
||||
.detail-header,
|
||||
.chat-panel,
|
||||
.info-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
line-height: 62rpx;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
color: #293247;
|
||||
font-size: 54rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 10rpx 24rpx rgba(83, 96, 130, 0.07);
|
||||
}
|
||||
|
||||
.button-hover {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.detail-title,
|
||||
.detail-subtitle,
|
||||
.info-label,
|
||||
.info-value {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
color: #20273a;
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.detail-subtitle {
|
||||
margin-top: 8rpx;
|
||||
color: #687286;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
min-height: 520rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 26rpx 20rpx 30rpx;
|
||||
border-radius: 30rpx;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(247, 251, 255, 0.8), rgba(255, 248, 252, 0.72)),
|
||||
rgba(255, 255, 255, 0.64);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.08);
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
width: 310rpx;
|
||||
margin: 0 auto 30rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #9aa3b2;
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
background: rgba(238, 243, 250, 0.88);
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.sent-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.reply-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
margin-bottom: 10rpx;
|
||||
color: #9aa3b2;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.reply-meta {
|
||||
margin-left: 2rpx;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
max-width: 560rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 29rpx;
|
||||
line-height: 1.68;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sent-bubble {
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 8rpx;
|
||||
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
|
||||
box-shadow: 0 12rpx 28rpx rgba(255, 120, 162, 0.16);
|
||||
}
|
||||
|
||||
.reply-avatar {
|
||||
width: 58rpx;
|
||||
height: 58rpx;
|
||||
line-height: 58rpx;
|
||||
margin-top: 34rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #ff78a2 0%, #83c6ff 100%);
|
||||
}
|
||||
|
||||
.reply-bubble {
|
||||
color: #333b4f;
|
||||
border-bottom-left-radius: 8rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid rgba(232, 238, 248, 0.88);
|
||||
box-shadow: 0 10rpx 24rpx rgba(93, 107, 139, 0.06);
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
margin-top: 24rpx;
|
||||
padding: 8rpx 26rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 18rpx 42rpx rgba(93, 107, 139, 0.08);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
min-height: 82rpx;
|
||||
border-bottom: 1rpx solid rgba(230, 236, 246, 0.72);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
flex-shrink: 0;
|
||||
color: #8c96a8;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #263044;
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user