fix:update README.md

This commit is contained in:
Zyronon
2025-11-11 22:59:09 +08:00
parent bf589dce92
commit 14c4a32403
14 changed files with 606 additions and 581 deletions

5
components.d.ts vendored
View File

@@ -39,8 +39,8 @@ declare module 'vue' {
IconFluentAddSquare20Regular: typeof import('~icons/fluent/add-square20-regular')['default']
IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default']
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-regular')['default']
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
IconFluentArrowShuffle20Filled: typeof import('~icons/fluent/arrow-shuffle20-filled')['default']
@@ -56,11 +56,11 @@ declare module 'vue' {
IconFluentChevronLeft28Filled: typeof import('~icons/fluent/chevron-left28-filled')['default']
IconFluentDatabasePerson20Regular: typeof import('~icons/fluent/database-person20-regular')['default']
IconFluentDelete20Regular: typeof import('~icons/fluent/delete20-regular')['default']
IconFluentDismiss12Regular: typeof import('~icons/fluent/dismiss12-regular')['default']
IconFluentDismiss20Regular: typeof import('~icons/fluent/dismiss20-regular')['default']
IconFluentDismissCircle16Regular: typeof import('~icons/fluent/dismiss-circle16-regular')['default']
IconFluentDismissCircle20Filled: typeof import('~icons/fluent/dismiss-circle20-filled')['default']
IconFluentErrorCircle20Filled: typeof import('~icons/fluent/error-circle20-filled')['default']
IconFluentErrorCircle20Regular: typeof import('~icons/fluent/error-circle20-regular')['default']
IconFluentEye16Regular: typeof import('~icons/fluent/eye16-regular')['default']
IconFluentEyeOff16Regular: typeof import('~icons/fluent/eye-off16-regular')['default']
IconFluentHome20Regular: typeof import('~icons/fluent/home20-regular')['default']
@@ -91,6 +91,7 @@ declare module 'vue' {
IconFluentWeatherMoon16Regular: typeof import('~icons/fluent/weather-moon16-regular')['default']
IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default']
IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default']
IconIxWechatLogo: typeof import('~icons/ix/wechat-logo')['default']
IconPhExportLight: typeof import('~icons/ph/export-light')['default']
IconSystemUiconsImport: typeof import('~icons/system-uicons/import')['default']
InputNumber: typeof import('./src/components/base/InputNumber.vue')['default']

View File

@@ -42,6 +42,7 @@
"@iconify-json/fluent": "^1.2.28",
"@iconify-json/icon-park-outline": "^1.2.4",
"@iconify-json/icon-park-solid": "^1.2.4",
"@iconify-json/ix": "^1.2.10",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify-json/oui": "^1.2.6",
"@iconify-json/ph": "^1.2.2",

68
pnpm-lock.yaml generated
View File

@@ -75,6 +75,9 @@ importers:
'@iconify-json/icon-park-solid':
specifier: ^1.2.4
version: 1.2.4
'@iconify-json/ix':
specifier: ^1.2.10
version: 1.2.10
'@iconify-json/material-symbols':
specifier: ^1.2.33
version: 1.2.33
@@ -171,9 +174,6 @@ importers:
vite-plugin-externals:
specifier: ^0.6.2
version: 0.6.2(vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(sass@1.90.0))
vite-plugin-mpa:
specifier: ^1.2.0
version: 1.2.0
vue-tsc:
specifier: ^3.0.1
version: 3.0.5(typescript@5.9.2)
@@ -517,6 +517,9 @@ packages:
'@iconify-json/icon-park-solid@1.2.4':
resolution: {integrity: sha512-030MChSP6lCY7N+U5J5R7YguHTGcm7qQEI/ivBjk77El/i8yJatoj568cwwXGM8c6HEU/kIxEE4m3O/6w0WBGg==}
'@iconify-json/ix@1.2.10':
resolution: {integrity: sha512-2NMqsW+sMyH+cpRnRW6mVqJM/q3Mbb7UVY9NWJJEJfHGn1SbzZde/jpgEmTZe5jMJMPQGWhaCzbGsTMrFim+3Q==}
'@iconify-json/material-symbols@1.2.33':
resolution: {integrity: sha512-Bs0X1+/vpJydW63olrGh60zkR8/Y70sI14AIWaP7Z6YQXukzWANH4q3I0sIPklbIn1oL6uwLvh0zQyd6Vh79LQ==}
@@ -1547,9 +1550,6 @@ packages:
cliui@3.2.0:
resolution: {integrity: sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==}
cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -1637,10 +1637,6 @@ packages:
confbox@0.2.2:
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
connect-history-api-fallback@1.6.0:
resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==}
engines: {node: '>=0.8'}
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
@@ -3176,11 +3172,6 @@ packages:
resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
engines: {node: '>=0.10.0'}
shelljs@0.8.5:
resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
engines: {node: '>=4'}
hasBin: true
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -3646,9 +3637,6 @@ packages:
peerDependencies:
vite: '>=2.0.0'
vite-plugin-mpa@1.2.0:
resolution: {integrity: sha512-A1G+CnnUkDuff2i+Z/RWeQMb8yj3FH9n7+KTEXxkOSeMRQ7v3Xy/tKtaMjPxW6n8zSOE/BbyzQAAX0RAoUd2AA==}
vite@7.1.2:
resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -3800,10 +3788,6 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -3811,10 +3795,6 @@ packages:
yargs-parser@5.0.1:
resolution: {integrity: sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==}
yargs@16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
@@ -4171,6 +4151,10 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/ix@1.2.10':
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/material-symbols@1.2.33':
dependencies:
'@iconify/types': 2.0.0
@@ -5415,12 +5399,6 @@ snapshots:
strip-ansi: 3.0.1
wrap-ansi: 2.1.0
cliui@7.0.4:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@@ -5517,8 +5495,6 @@ snapshots:
confbox@0.2.2: {}
connect-history-api-fallback@1.6.0: {}
consola@3.4.2: {}
content-type@1.0.5: {}
@@ -7159,12 +7135,6 @@ snapshots:
is-plain-object: 2.0.4
split-string: 3.1.0
shelljs@0.8.5:
dependencies:
glob: 7.2.3
interpret: 1.4.0
rechoir: 0.6.2
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -7720,12 +7690,6 @@ snapshots:
magic-string: 0.25.9
vite: 7.1.2(@types/node@24.3.0)(jiti@2.5.1)(sass@1.90.0)
vite-plugin-mpa@1.2.0:
dependencies:
connect-history-api-fallback: 1.6.0
shelljs: 0.8.5
yargs: 16.2.0
vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(sass@1.90.0):
dependencies:
esbuild: 0.25.9
@@ -7845,8 +7809,6 @@ snapshots:
yallist@3.1.1: {}
yargs-parser@20.2.9: {}
yargs-parser@21.1.1: {}
yargs-parser@5.0.1:
@@ -7854,16 +7816,6 @@ snapshots:
camelcase: 3.0.0
object.assign: 4.1.7
yargs@16.2.0:
dependencies:
cliui: 7.0.4
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 20.2.9
yargs@17.7.2:
dependencies:
cliui: 8.0.1

100
public/privacy-policy.html Normal file
View File

@@ -0,0 +1,100 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>隐私政策</title>
</head>
<body
style="display:flex;justify-content:center">
<div class="privacy-page"
style="width: 60vw;"
>
<h1 style="text-align: center">隐私政策</h1>
<div class="content">
<section>
<h2>一、引言</h2>
<p>
我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用、存储和保护您的个人信息。在使用本应用之前,请您仔细阅读本隐私政策。</p>
</section>
<section>
<h2>二、信息收集</h2>
<p>我们可能收集以下信息:</p>
<p><strong>1. 账户信息:</strong>当您注册账户时,我们会收集您的手机号、邮箱地址、密码等信息。</p>
<p><strong>2. 学习数据:</strong>我们会记录您的学习进度、学习记录、练习数据等信息,以便为您提供个性化的学习服务。
</p>
<p><strong>3. 设备信息:</strong>我们可能收集您的设备型号、操作系统版本、唯一设备标识符等信息,用于改善服务质量和安全性。
</p>
<p><strong>4. 日志信息:</strong>当您使用本应用时我们可能自动收集某些信息包括IP地址、访问时间、访问页面等。
</p>
</section>
<section>
<h2>三、信息使用</h2>
<p>我们使用收集的信息用于以下目的:</p>
<p>1. 提供、维护和改进我们的服务;</p>
<p>2. 处理您的注册、登录、学习记录等请求;</p>
<p>3. 向您发送服务通知、更新和安全提醒;</p>
<p>4. 进行数据分析,以改善用户体验和服务质量;</p>
<p>5. 检测、预防和解决技术问题;</p>
<p>6. 遵守法律法规要求。</p>
</section>
<section>
<h2>四、信息存储</h2>
<p>1. 我们采用行业标准的安全措施来保护您的个人信息,防止未经授权的访问、使用或泄露。</p>
<p>2. 您的个人信息将存储在安全的服务器上,我们会对数据进行加密处理。</p>
<p>3. 我们仅在为实现本隐私政策所述目的所必需的期间内保留您的个人信息。</p>
</section>
<section>
<h2>五、信息共享</h2>
<p>我们不会向第三方出售、交易或转让您的个人信息,除非:</p>
<p>1. 获得您的明确同意;</p>
<p>2. 法律法规要求或司法机关、行政机关依法要求提供;</p>
<p>3. 为履行我们的服务协议或本隐私政策,我们可能需要与我们的服务提供商共享某些信息。</p>
</section>
<section>
<h2>六、Cookie和类似技术</h2>
<p>
我们可能使用Cookie和类似技术来收集信息、改善用户体验和分析服务使用情况。您可以通过浏览器设置管理Cookie但这可能影响某些功能的正常使用。</p>
</section>
<section>
<h2>七、您的权利</h2>
<p>根据相关法律法规,您对自己的个人信息享有以下权利:</p>
<p>1. <strong>访问权:</strong>您有权访问我们持有的关于您的个人信息;</p>
<p>2. <strong>更正权:</strong>您有权要求更正不准确的个人信息;</p>
<p>3. <strong>删除权:</strong>在特定情况下,您有权要求删除您的个人信息;</p>
<p>4. <strong>撤回同意:</strong>您有权随时撤回您之前给予的同意;</p>
<p>5. <strong>投诉权:</strong>如果您认为我们对您个人信息的处理违反了相关法律法规,您有权向相关监管部门投诉。
</p>
</section>
<section>
<h2>八、未成年人保护</h2>
<p>
我们非常重视未成年人的个人信息保护。如果您是未成年人,建议您请您的父母或监护人仔细阅读本隐私政策,并在征得您的父母或监护人同意的前提下使用我们的服务。</p>
</section>
<section>
<h2>九、隐私政策更新</h2>
<p>
我们可能会不时更新本隐私政策。我们会在本页面上发布新的隐私政策,并通过适当方式通知您。如果您不同意更新后的隐私政策,您可以选择停止使用我们的服务。</p>
</section>
<section>
<h2>十、联系我们</h2>
<p>如果您对本隐私政策有任何疑问、意见或建议,或需要行使您的相关权利,请通过以下方式联系我们:</p>
<p>邮箱zyronon@163.com</p>
</section>
<div class="update-time">
<p>最后更新时间2025年11月11日</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,83 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>用户协议</title>
</head>
<body
style="display:flex;justify-content:center">
<div class="privacy-page"
style="width: 60vw;"
>
<h1 style="text-align: center">用户协议</h1>
<div class="content">
<section>
<h2>一、总则</h2>
<p>欢迎使用本应用!在使用本应用之前,请您仔细阅读本用户协议(以下简称"本协议")。当您注册、登录、使用(以下统称"使用")本应用时,即表示您已阅读、理解并同意接受本协议的全部内容。</p>
</section>
<section>
<h2>二、服务内容</h2>
<p>本应用为用户提供单词学习、文章阅读等在线教育服务。我们保留随时修改或中断服务而不需通知用户的权利,我们行使修改或中断服务的权利,不需对用户或第三方负责。</p>
</section>
<section>
<h2>三、用户账户</h2>
<p>1. 用户在使用本应用前需要注册一个账户。用户应当使用真实、准确、完整的信息注册账户。</p>
<p>2. 用户有责任维护账户信息的安全,对账户下的所有活动负责。</p>
<p>3. 用户不得将账户转让、出售或以其他方式提供给第三方使用。</p>
</section>
<section>
<h2>四、用户行为规范</h2>
<p>用户在使用本应用时,应当遵守相关法律法规,不得从事以下行为:</p>
<p>1. 发布、传播违法、有害、威胁、辱骂、骚扰、侵权、诽谤、淫秽、暴力或其他不当内容;</p>
<p>2. 侵犯他人知识产权、隐私权或其他合法权益;</p>
<p>3. 干扰或破坏本应用的正常运行;</p>
<p>4. 使用自动化工具或脚本进行数据采集、批量操作等;</p>
<p>5. 其他违反法律法规或本协议的行为。</p>
</section>
<section>
<h2>五、知识产权</h2>
<p>1. 本应用的所有内容,包括但不限于文字、图片、音频、视频、软件、程序、版面设计等,均受知识产权法保护。</p>
<p>2. 未经我们书面许可,用户不得复制、传播、展示、镜像、上传、下载本应用的任何内容。</p>
</section>
<section>
<h2>六、隐私保护</h2>
<p>我们重视用户的隐私保护。关于我们如何收集、使用、存储和保护您的个人信息,请详见《隐私政策》。</p>
</section>
<section>
<h2>七、免责声明</h2>
<p>1. 用户明确同意使用本应用的风险由用户个人承担。</p>
<p>2. 我们不对因不可抗力或非我们原因造成的服务中断或终止承担责任。</p>
<p>3. 我们不对用户在使用本应用过程中产生的任何直接、间接、偶然、特殊及后续的损害承担责任。</p>
</section>
<section>
<h2>八、协议修改</h2>
<p>我们有权随时修改本协议的任何条款。一旦本协议的内容发生变动,我们将会通过适当方式向用户提示修改内容。如果用户不同意我们对本协议相关条款所做的修改,用户有权停止使用本应用。如果用户继续使用本应用,则视为用户接受我们对本协议相关条款所做的修改。</p>
</section>
<section>
<h2>九、法律适用与争议解决</h2>
<p>1. 本协议的订立、执行和解释及争议的解决均应适用中华人民共和国法律。</p>
<p>2. 如双方就本协议内容或其执行发生任何争议,双方应尽量友好协商解决;协商不成时,任何一方均可向我们所在地的人民法院提起诉讼。</p>
</section>
<section>
<h2>十、其他</h2>
<p>1. 本协议构成双方对本协议之约定事项及其他有关事宜的完整协议,除本协议规定的之外,未赋予本协议各方其他权利。</p>
<p>2. 如本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,本协议的其余条款仍应有效并且有约束力。</p>
</section>
<div class="update-time">
<p>最后更新时间2025年11月11日</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -48,7 +48,7 @@ export function addDict(params?, data?) {
return http<Dict>('dict/addDict', remove(data), remove(params), 'post')
}
export function uploadImportData(data,onUploadProgress) {
export function uploadImportData(data, onUploadProgress) {
return axiosInstance({
url: 'dict/uploadImportData',
method: 'post',
@@ -60,5 +60,7 @@ export function uploadImportData(data,onUploadProgress) {
})
}
// 导出认证相关API
export * from './auth'
// 查询导入进度status: 0=导入中, 1=完成, 2=失败
export function getProgress(params?) {
return http<{ status: number; reason: string }>('dict/getProgress', null, params, 'get')
}

View File

@@ -1,11 +1,12 @@
import http from '@/utils/http.ts'
// 用户登录接口
export interface LoginParams {
email?: string
phone?: string
account?: string
password?: string
phone?: string
code?: string
type: 'email' | 'phone' | 'wechat'
type: 'code' | 'pwd'
}
export interface LoginResponse {
@@ -61,28 +62,28 @@ export interface WechatLoginParams {
}
// API 函数定义
export function login(params: LoginParams) {
export function loginApi(params: LoginParams) {
// 暂时直接返回成功响应,等待后端接入
return Promise.resolve({
success: true,
code: 200,
msg: '登录成功',
data: {
token: 'mock_token_' + Date.now(),
user: {
id: '1',
email: params.email,
phone: params.phone,
nickname: '测试用户',
avatar: ''
}
}
})
// return http<LoginResponse>('auth/login', params, null, 'post')
// return Promise.resolve({
// success: true,
// code: 200,
// msg: '登录成功',
// data: {
// token: 'mock_token_' + Date.now(),
// user: {
// id: '1',
// account: params.account ?? 'account',
// phone: params.phone ?? 'phone',
// nickname: '测试用户',
// avatar: ''
// }
// }
// })
return http<LoginResponse>('user/login', params, null, 'post')
}
export function register(params: RegisterParams) {
return http<RegisterResponse>('auth/register', params, null, 'post')
export function registerApi(params: RegisterParams) {
return http<RegisterResponse>('user/register', params, null, 'post')
}
export function sendCode(params: SendCodeParams) {
@@ -91,26 +92,26 @@ export function sendCode(params: SendCodeParams) {
code: 200,
msg: '登录成功',
})
return http<boolean>('auth/sendCode', params, null, 'post')
return http<boolean>('user/sendCode', params, null, 'post')
}
export function resetPassword(params: ResetPasswordParams) {
return http<boolean>('auth/resetPassword', params, null, 'post')
export function resetPasswordApi(params: ResetPasswordParams) {
return http<boolean>('user/resetPassword', params, null, 'post')
}
export function wechatLogin(params: WechatLoginParams) {
return http<LoginResponse>('auth/wechatLogin', params, null, 'post')
return http<LoginResponse>('user/wechatLogin', params, null, 'post')
}
export function logout() {
return http<boolean>('auth/logout', null, null, 'post')
export function logoutApi() {
return http<boolean>('user/logout', null, null, 'post')
}
export function refreshToken() {
return http<{ token: string }>('auth/refreshToken', null, null, 'post')
return http<{ token: string }>('user/refreshToken', null, null, 'post')
}
// 获取用户信息
export function getUserInfo() {
return http<LoginResponse['user']>('auth/userInfo', null, null, 'get')
return http<LoginResponse['user']>('user/userInfo', null, null, 'get')
}

View File

@@ -6,9 +6,9 @@
<div class="h-12 text-xs text-gray-400">
<span>
继续操作即表示你阅读并同意我们的
<router-link to="/user-agreement" className="link">用户协议</router-link>
<a href="/user-agreement.html" target="_blank" class="link">用户协议</a>
<router-link to="/privacy-policy" className="link">隐私政策</router-link>
<a href="/privacy-policy.html" target="_blank" class="link">隐私政策</a>
</span>
<slot/>
</div>

View File

@@ -1,144 +0,0 @@
<script setup lang="ts">
import BasePage from "@/components/BasePage.vue";
import BackIcon from "@/components/BackIcon.vue";
</script>
<template>
<BasePage>
<div class="privacy-page">
<BackIcon />
<h1 class="page-title">隐私政策</h1>
<div class="content">
<section>
<h2>引言</h2>
<p>我们非常重视您的隐私保护本隐私政策说明了我们如何收集使用存储和保护您的个人信息在使用本应用之前请您仔细阅读本隐私政策</p>
</section>
<section>
<h2>信息收集</h2>
<p>我们可能收集以下信息</p>
<p><strong>1. 账户信息</strong>当您注册账户时我们会收集您的手机号邮箱地址密码等信息</p>
<p><strong>2. 学习数据</strong>我们会记录您的学习进度学习记录练习数据等信息以便为您提供个性化的学习服务</p>
<p><strong>3. 设备信息</strong>我们可能收集您的设备型号操作系统版本唯一设备标识符等信息用于改善服务质量和安全性</p>
<p><strong>4. 日志信息</strong>当您使用本应用时我们可能自动收集某些信息包括IP地址访问时间访问页面等</p>
</section>
<section>
<h2>信息使用</h2>
<p>我们使用收集的信息用于以下目的</p>
<p>1. 提供维护和改进我们的服务</p>
<p>2. 处理您的注册登录学习记录等请求</p>
<p>3. 向您发送服务通知更新和安全提醒</p>
<p>4. 进行数据分析以改善用户体验和服务质量</p>
<p>5. 检测预防和解决技术问题</p>
<p>6. 遵守法律法规要求</p>
</section>
<section>
<h2>信息存储</h2>
<p>1. 我们采用行业标准的安全措施来保护您的个人信息防止未经授权的访问使用或泄露</p>
<p>2. 您的个人信息将存储在安全的服务器上我们会对数据进行加密处理</p>
<p>3. 我们仅在为实现本隐私政策所述目的所必需的期间内保留您的个人信息</p>
</section>
<section>
<h2>信息共享</h2>
<p>我们不会向第三方出售交易或转让您的个人信息除非</p>
<p>1. 获得您的明确同意</p>
<p>2. 法律法规要求或司法机关行政机关依法要求提供</p>
<p>3. 为履行我们的服务协议或本隐私政策我们可能需要与我们的服务提供商共享某些信息</p>
</section>
<section>
<h2>Cookie和类似技术</h2>
<p>我们可能使用Cookie和类似技术来收集信息改善用户体验和分析服务使用情况您可以通过浏览器设置管理Cookie但这可能影响某些功能的正常使用</p>
</section>
<section>
<h2>您的权利</h2>
<p>根据相关法律法规您对自己的个人信息享有以下权利</p>
<p>1. <strong>访问权</strong>您有权访问我们持有的关于您的个人信息</p>
<p>2. <strong>更正权</strong>您有权要求更正不准确的个人信息</p>
<p>3. <strong>删除权</strong>在特定情况下您有权要求删除您的个人信息</p>
<p>4. <strong>撤回同意</strong>您有权随时撤回您之前给予的同意</p>
<p>5. <strong>投诉权</strong>如果您认为我们对您个人信息的处理违反了相关法律法规您有权向相关监管部门投诉</p>
</section>
<section>
<h2>未成年人保护</h2>
<p>我们非常重视未成年人的个人信息保护如果您是未成年人建议您请您的父母或监护人仔细阅读本隐私政策并在征得您的父母或监护人同意的前提下使用我们的服务</p>
</section>
<section>
<h2>隐私政策更新</h2>
<p>我们可能会不时更新本隐私政策我们会在本页面上发布新的隐私政策并通过适当方式通知您如果您不同意更新后的隐私政策您可以选择停止使用我们的服务</p>
</section>
<section>
<h2>联系我们</h2>
<p>如果您对本隐私政策有任何疑问意见或建议或需要行使您的相关权利请通过以下方式联系我们</p>
<p>邮箱privacy@example.com</p>
</section>
<div class="update-time">
<p>最后更新时间2024年1月1日</p>
</div>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
.privacy-page {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
.page-title {
font-size: 2rem;
font-weight: 600;
margin: 2rem 0;
text-align: center;
color: var(--color-text);
}
.content {
line-height: 1.8;
color: var(--color-text);
section {
margin-bottom: 2rem;
h2 {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--color-text);
}
p {
margin-bottom: 0.8rem;
text-align: justify;
color: var(--color-text-secondary, #666);
strong {
color: var(--color-text);
}
}
}
}
.update-time {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border, #eee);
text-align: center;
p {
color: var(--color-text-secondary, #999);
font-size: 0.9rem;
}
}
</style>

View File

@@ -1,131 +0,0 @@
<script setup lang="ts">
import BasePage from "@/components/BasePage.vue";
import BackIcon from "@/components/BackIcon.vue";
</script>
<template>
<BasePage>
<div class="agreement-page">
<BackIcon />
<h1 class="page-title">用户协议</h1>
<div class="content">
<section>
<h2>总则</h2>
<p>欢迎使用本应用在使用本应用之前请您仔细阅读本用户协议以下简称"本协议"当您注册登录使用以下统称"使用"本应用时即表示您已阅读理解并同意接受本协议的全部内容</p>
</section>
<section>
<h2>服务内容</h2>
<p>本应用为用户提供单词学习文章阅读等在线教育服务我们保留随时修改或中断服务而不需通知用户的权利我们行使修改或中断服务的权利不需对用户或第三方负责</p>
</section>
<section>
<h2>用户账户</h2>
<p>1. 用户在使用本应用前需要注册一个账户用户应当使用真实准确完整的信息注册账户</p>
<p>2. 用户有责任维护账户信息的安全对账户下的所有活动负责</p>
<p>3. 用户不得将账户转让出售或以其他方式提供给第三方使用</p>
</section>
<section>
<h2>用户行为规范</h2>
<p>用户在使用本应用时应当遵守相关法律法规不得从事以下行为</p>
<p>1. 发布传播违法有害威胁辱骂骚扰侵权诽谤淫秽暴力或其他不当内容</p>
<p>2. 侵犯他人知识产权隐私权或其他合法权益</p>
<p>3. 干扰或破坏本应用的正常运行</p>
<p>4. 使用自动化工具或脚本进行数据采集批量操作等</p>
<p>5. 其他违反法律法规或本协议的行为</p>
</section>
<section>
<h2>知识产权</h2>
<p>1. 本应用的所有内容包括但不限于文字图片音频视频软件程序版面设计等均受知识产权法保护</p>
<p>2. 未经我们书面许可用户不得复制传播展示镜像上传下载本应用的任何内容</p>
</section>
<section>
<h2>隐私保护</h2>
<p>我们重视用户的隐私保护关于我们如何收集使用存储和保护您的个人信息请详见隐私政策</p>
</section>
<section>
<h2>免责声明</h2>
<p>1. 用户明确同意使用本应用的风险由用户个人承担</p>
<p>2. 我们不对因不可抗力或非我们原因造成的服务中断或终止承担责任</p>
<p>3. 我们不对用户在使用本应用过程中产生的任何直接间接偶然特殊及后续的损害承担责任</p>
</section>
<section>
<h2>协议修改</h2>
<p>我们有权随时修改本协议的任何条款一旦本协议的内容发生变动我们将会通过适当方式向用户提示修改内容如果用户不同意我们对本协议相关条款所做的修改用户有权停止使用本应用如果用户继续使用本应用则视为用户接受我们对本协议相关条款所做的修改</p>
</section>
<section>
<h2>法律适用与争议解决</h2>
<p>1. 本协议的订立执行和解释及争议的解决均应适用中华人民共和国法律</p>
<p>2. 如双方就本协议内容或其执行发生任何争议双方应尽量友好协商解决协商不成时任何一方均可向我们所在地的人民法院提起诉讼</p>
</section>
<section>
<h2>其他</h2>
<p>1. 本协议构成双方对本协议之约定事项及其他有关事宜的完整协议除本协议规定的之外未赋予本协议各方其他权利</p>
<p>2. 如本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力本协议的其余条款仍应有效并且有约束力</p>
</section>
<div class="update-time">
<p>最后更新时间2024年1月1日</p>
</div>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
.agreement-page {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
.page-title {
font-size: 2rem;
font-weight: 600;
margin: 2rem 0;
text-align: center;
color: var(--color-text);
}
.content {
line-height: 1.8;
color: var(--color-text);
section {
margin-bottom: 2rem;
h2 {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--color-text);
}
p {
margin-bottom: 0.8rem;
text-align: justify;
color: var(--color-text-secondary, #666);
}
}
}
.update-time {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border, #eee);
text-align: center;
p {
color: var(--color-text-secondary, #999);
font-size: 0.9rem;
}
}
</style>

View File

@@ -4,94 +4,219 @@ import { useAuthStore } from "@/stores/auth.ts";
import { useRouter } from "vue-router";
import BaseButton from "@/components/BaseButton.vue";
import Toast from "@/components/base/toast/Toast.ts";
import { uploadImportData, getProgress } from "@/apis/index.ts";
const authStore = useAuthStore()
const router = useRouter()
const authStore = useAuthStore();
const router = useRouter();
// 页面状态
const isLoading = ref(false)
const isLoading = ref(false);
// 同步数据状态
const isSyncing = ref(false);
const uploadPercent = ref(0);
const progressText = ref("等待上传...");
const syncStatus = ref<number | null>(null); // 0=导入中,1=完成,2=失败
const syncReason = ref("");
const fileInputRef = ref<HTMLInputElement | null>(null);
// 退出登录
const handleLogout = async () => {
await authStore.logout()
}
isLoading.value = true;
try {
await authStore.logout();
} finally {
isLoading.value = false;
}
};
// 跳转到设置页面
const goToSettings = () => {
router.push('/setting')
}
router.push("/setting");
};
onMounted(() => {
// 如果用户未登录,跳转到登录页
if (!authStore.isLoggedIn) {
router.push({path: "/login"});
return
return;
}
// 获取用户信息
if (!authStore.user) {
authStore.fetchUserInfo()
authStore.fetchUserInfo();
}
})
});
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const resetSync = () => {
isSyncing.value = false;
uploadPercent.value = 0;
progressText.value = "等待上传...";
syncStatus.value = null;
syncReason.value = "";
};
const handleSyncClick = () => {
fileInputRef.value?.click();
};
const onFileSelected = async (e: Event) => {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
input.value = ""; // 重置,便于重复选择同一文件
if (!file) return;
const ext = file.name.split(".").pop()?.toLowerCase();
if (!ext || (ext !== "zip" && ext !== "json")) {
Toast.warning("仅支持上传 zip 或 json 文件");
return;
}
try {
isSyncing.value = true;
progressText.value = "上传中...";
const formData = new FormData();
formData.append("file", file);
await uploadImportData(formData, (event: ProgressEvent) => {
if (event.total) {
uploadPercent.value = Math.round((event.loaded / event.total) * 100);
}
});
progressText.value = "导入中...";
// 轮询导入进度,直到 status != 0
while (true) {
const res = await getProgress();
const { status, reason } = res as any; // http 封装返回结构按实际为准
syncStatus.value = status;
syncReason.value = reason || "";
if (status !== 0) break;
await sleep(1000);
}
if (syncStatus.value === 1) {
uploadPercent.value = 100;
progressText.value = "导入完成";
Toast.success("数据同步成功");
} else if (syncStatus.value === 2) {
progressText.value = "导入失败";
Toast.error(syncReason.value || "导入失败");
}
} catch (err: any) {
progressText.value = "上传或导入失败";
Toast.error(err?.message || "上传失败");
} finally {
// 保留结果展示片刻,再复位
setTimeout(() => resetSync(), 1500);
}
};
</script>
<template>
<div class="user-center">
<div class="user-header">
<div class="avatar">
<img v-if="authStore.user?.avatar" :src="authStore.user.avatar" alt="头像" />
<div v-else class="avatar-placeholder">
{{ authStore.user?.nickname?.charAt(0) || 'U' }}
<div class="user-page">
<div class="profile-card">
<div class="profile-header">
<div class="avatar-wrap">
<div class="avatar ring">
<img v-if="authStore.user?.avatar" :src="authStore.user.avatar" alt="头像" />
<div v-else class="avatar-placeholder">
{{ authStore.user?.nickname?.charAt(0) || "U" }}
</div>
</div>
</div>
<div class="headline">
<h2>{{ authStore.user?.nickname || "用户" }}</h2>
<p v-if="authStore.user?.email">{{ authStore.user.email }}</p>
<p v-if="authStore.user?.phone">{{ authStore.user.phone }}</p>
</div>
</div>
<div class="user-info">
<h2>{{ authStore.user?.nickname || '用户' }}</h2>
<p v-if="authStore.user?.email">{{ authStore.user.email }}</p>
<p v-if="authStore.user?.phone">{{ authStore.user.phone }}</p>
</div>
</div>
<div class="user-actions">
<BaseButton @click="goToSettings" class="w-full mb-4" size="large">
系统设置
</BaseButton>
<BaseButton
@click="handleLogout"
type="info"
class="w-full"
size="large"
:loading="isLoading"
>
退出登录
</BaseButton>
<div class="actions">
<BaseButton
class="w-full"
size="large"
type="primary"
:disabled="isSyncing"
@click="handleSyncClick"
>
同步数据
</BaseButton>
<BaseButton class="w-full" size="large" @click="goToSettings">
系统设置
</BaseButton>
<BaseButton
class="w-full"
size="large"
type="info"
:loading="isLoading"
@click="handleLogout"
>
退出登录
</BaseButton>
</div>
<input
ref="fileInputRef"
type="file"
accept=".zip,.json"
class="hidden"
@change="onFileSelected"
/>
<div v-if="isSyncing" class="sync-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: uploadPercent + '%' }"></div>
</div>
<div class="progress-text">
<span>{{ progressText }}</span>
<span v-if="syncStatus === 2 && syncReason" class="reason">{{ syncReason }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.user-center {
max-width: 600px;
.user-page {
max-width: 760px;
margin: 0 auto;
padding: 2rem;
padding: 2rem 1.25rem 3rem;
}
.user-header {
.profile-card {
background: #fff;
border-radius: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.profile-header {
position: relative;
padding: 2.25rem 2rem 1.5rem;
background: linear-gradient(135deg, #6b73ff 0%, #000dff 100%);
color: #fff;
display: flex;
align-items: center;
gap: 1.5rem;
padding: 2rem 0;
border-bottom: 1px solid #eee;
margin-bottom: 2rem;
gap: 1.25rem;
}
.avatar-wrap {
display: flex;
align-items: center;
justify-content: center;
}
.avatar {
width: 80px;
height: 80px;
width: 88px;
height: 88px;
border-radius: 50%;
overflow: hidden;
border: 3px solid var(--color-select-bg);
background: #fff1;
img {
width: 100%;
height: 100%;
@@ -99,6 +224,18 @@ onMounted(() => {
}
}
.ring {
position: relative;
}
.ring::before {
content: "";
position: absolute;
inset: -4px;
border-radius: 50%;
background: linear-gradient(135deg, #fff, rgba(255, 255, 255, 0.2));
-webkit-mask: radial-gradient(circle at center, transparent 62%, #000 63%);
}
.avatar-placeholder {
width: 100%;
height: 100%;
@@ -111,29 +248,50 @@ onMounted(() => {
font-weight: bold;
}
.user-info {
flex: 1;
h2 {
margin: 0 0 0.5rem 0;
color: #333;
font-size: 1.5rem;
}
p {
margin: 0.25rem 0;
color: #666;
font-size: 0.9rem;
}
.headline h2 {
margin: 0 0 0.25rem 0;
font-size: 1.6rem;
font-weight: 700;
}
.headline p {
margin: 0.1rem 0;
opacity: 0.9;
}
.user-actions {
.actions {
display: grid;
grid-template-columns: 1fr;
gap: 0.9rem;
padding: 1.25rem;
}
.sync-progress {
padding: 0 1.25rem 1.25rem;
}
.progress-bar {
width: 100%;
height: 10px;
border-radius: 999px;
background: #f1f2f6;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #6b73ff 0%, #00d4ff 100%);
transition: width 0.3s ease;
}
.progress-text {
display: flex;
flex-direction: column;
gap: 1rem;
.mb-4 {
margin-bottom: 1rem;
}
justify-content: space-between;
margin-top: 0.5rem;
font-size: 0.9rem;
color: #444;
}
.reason {
color: #d33;
}
.hidden {
display: none;
}
</style>

View File

@@ -1,18 +1,18 @@
<script setup lang="tsx">
import {onBeforeUnmount, onMounted} from 'vue'
import {useRoute} from 'vue-router'
import { onBeforeUnmount, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import BaseInput from "@/components/base/BaseInput.vue";
import BaseButton from "@/components/BaseButton.vue";
import {APP_NAME} from "@/config/env.ts";
import {useAuthStore} from "@/stores/auth.ts";
import {sendCode} from "@/apis";
import {validateEmail, validatePhone} from "@/utils/validation.ts";
import { APP_NAME } from "@/config/env.ts";
import { useAuthStore } from "@/stores/auth.ts";
import { sendCode } from "@/apis/user.ts";
import { validateEmail, validatePhone } from "@/utils/validation.ts";
import Toast from "@/components/base/toast/Toast.ts";
import FormItem from "@/components/base/form/FormItem.vue";
import Form from "@/components/base/form/Form.vue";
import Notice from "@/pages/user/Notice.vue";
import {FormInstance} from "@/components/base/form/types.ts";
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
import { FormInstance } from "@/components/base/form/types.ts";
import { PASSWORD_CONFIG, PHONE_CONFIG } from "@/config/auth.ts";
// 状态管理
const authStore = useAuthStore()
@@ -176,20 +176,20 @@ async function sendVerificationCode(phone: string, type: 'login' | 'register' |
// 统一登录处理
async function handleLogin() {
await currentFormRef.validate(async (valid) => {
currentFormRef.validate(async (valid) => {
if (!valid) return;
//手机号登录
if (loginType === 'code') {
await authStore.login({
phone: phoneLoginForm.phone,
code: phoneLoginForm.code,
type: 'phone'
type: 'code'
})
} else {
await authStore.login({
account: loginForm.account,
password: loginForm.password,
type: 'account'
type: 'pwd'
})
}
})
@@ -197,7 +197,7 @@ async function handleLogin() {
// 注册
async function handleRegister() {
await registerFormRef.validate(async (valid) => {
registerFormRef.validate(async (valid) => {
if (!valid) return
await authStore.register({
phone: registerForm.phone,
@@ -211,10 +211,10 @@ async function handleRegister() {
// 忘记密码
async function handleForgotPassword() {
await forgotFormRef.validate(async (valid) => {
forgotFormRef.validate(async (valid) => {
if (!valid) return
const response = await authStore.resetPassword({
phone: forgotForm.phone,
phone: forgotForm.account,
email: undefined,
code: forgotForm.code,
newPassword: forgotForm.newPassword
@@ -335,28 +335,28 @@ onBeforeUnmount(() => {
<!-- Tab切换 -->
<div class="center gap-8 mb-6">
<div
class="center cp transition-colors"
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'code'"
class="center cp transition-colors"
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'code'"
>
<div>
<span>验证码登录</span>
<div
v-opacity="loginType === 'code'"
class="mt-1 h-0.5 bg-blue-600"
v-opacity="loginType === 'code'"
class="mt-1 h-0.5 bg-blue-600"
></div>
</div>
</div>
<div
class="center cp transition-colors"
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'password'"
class="center cp transition-colors"
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'password'"
>
<div>
<span>密码登录</span>
<div
v-opacity="loginType === 'password'"
class="mt-1 h-0.5 bg-blue-600"
v-opacity="loginType === 'password'"
class="mt-1 h-0.5 bg-blue-600"
></div>
</div>
</div>
@@ -364,10 +364,10 @@ onBeforeUnmount(() => {
<!-- 验证码登录表单 -->
<Form
v-if="loginType === 'code'"
ref="phoneLoginFormRef"
:rules="phoneLoginFormRules"
:model="phoneLoginForm">
v-if="loginType === 'code'"
ref="phoneLoginFormRef"
:rules="phoneLoginFormRules"
:model="phoneLoginForm">
<FormItem prop="phone">
<BaseInput v-model="phoneLoginForm.phone"
type="tel"
@@ -378,18 +378,18 @@ onBeforeUnmount(() => {
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="phoneLoginForm.code"
type="text"
size="large"
:max-length="PHONE_CONFIG.codeLength"
placeholder="请输入验证码"
v-model="phoneLoginForm.code"
type="text"
size="large"
:max-length="PHONE_CONFIG.codeLength"
placeholder="请输入验证码"
/>
<BaseButton
@click="sendVerificationCode(phoneLoginForm.phone, 'login','phone')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
@click="sendVerificationCode(phoneLoginForm.phone, 'login','phone')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '发送验证码') }}
</BaseButton>
@@ -399,10 +399,10 @@ onBeforeUnmount(() => {
<!-- 密码登录表单 -->
<Form
v-else
ref="loginForm2Ref"
:rules="loginForm2Rules"
:model="loginForm2">
v-else
ref="loginForm2Ref"
:rules="loginForm2Rules"
:model="loginForm2">
<FormItem prop="account">
<BaseInput v-model="loginForm2.account"
type="text"
@@ -413,10 +413,10 @@ onBeforeUnmount(() => {
<FormItem prop="password">
<div class="flex gap-2">
<BaseInput
v-model="loginForm2.password"
type="password"
size="large"
placeholder="请输入密码"
v-model="loginForm2.password"
type="password"
size="large"
placeholder="请输入密码"
/>
</div>
</FormItem>
@@ -427,10 +427,10 @@ onBeforeUnmount(() => {
</Notice>
<BaseButton
class="w-full"
size="large"
:loading="authStore.isLoading"
@click="handleLogin"
class="w-full"
size="large"
:loading="authStore.isLoading"
@click="handleLogin"
>
登录
</BaseButton>
@@ -446,32 +446,32 @@ onBeforeUnmount(() => {
<div v-else-if="currentMode === 'register'">
<div class="mb-6 text-xl font-bold text-center">注册新账号</div>
<Form
ref="registerFormRef"
:rules="registerFormRules"
:model="registerForm">
ref="registerFormRef"
:rules="registerFormRules"
:model="registerForm">
<FormItem prop="phone">
<BaseInput
v-model="registerForm.phone"
type="tel"
size="large"
placeholder="请输入手机号"
v-model="registerForm.phone"
type="tel"
size="large"
placeholder="请输入手机号"
/>
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="registerForm.code"
type="text"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
v-model="registerForm.code"
type="text"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<BaseButton
@click="sendVerificationCode(registerForm.phone, 'register','phone')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
@click="sendVerificationCode(registerForm.phone, 'register','phone')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }}
</BaseButton>
@@ -479,18 +479,18 @@ onBeforeUnmount(() => {
</FormItem>
<FormItem prop="password">
<BaseInput
v-model="registerForm.password"
type="password"
size="large"
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
v-model="registerForm.password"
type="password"
size="large"
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
/>
</FormItem>
<FormItem prop="confirmPassword">
<BaseInput
v-model="registerForm.confirmPassword"
type="password"
size="large"
placeholder="请再次输入密码"
v-model="registerForm.confirmPassword"
type="password"
size="large"
placeholder="请再次输入密码"
/>
</FormItem>
</Form>
@@ -498,10 +498,10 @@ onBeforeUnmount(() => {
<Notice/>
<BaseButton
class="w-full"
size="large"
:loading="authStore.isLoading"
@click="handleRegister"
class="w-full"
size="large"
:loading="authStore.isLoading"
@click="handleRegister"
>
注册
</BaseButton>
@@ -516,32 +516,32 @@ onBeforeUnmount(() => {
<div v-else-if="currentMode === 'forgot'">
<div class="mb-6 text-xl font-bold text-center">重置密码</div>
<Form
ref="forgotFormRef"
:rules="forgotFormRules"
:model="forgotForm">
ref="forgotFormRef"
:rules="forgotFormRules"
:model="forgotForm">
<FormItem prop="account">
<BaseInput
v-model="forgotForm.account"
type="tel"
size="large"
placeholder="请输入手机号/邮箱地址"
v-model="forgotForm.account"
type="tel"
size="large"
placeholder="请输入手机号/邮箱地址"
/>
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="forgotForm.code"
type="text"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
v-model="forgotForm.code"
type="text"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<BaseButton
@click="sendVerificationCode(forgotForm.account, 'reset_password','account')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
@click="sendVerificationCode(forgotForm.account, 'reset_password','account')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }}
</BaseButton>
@@ -549,27 +549,27 @@ onBeforeUnmount(() => {
</FormItem>
<FormItem prop="newPassword">
<BaseInput
v-model="forgotForm.newPassword"
type="password"
size="large"
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
v-model="forgotForm.newPassword"
type="password"
size="large"
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
/>
</FormItem>
<FormItem prop="confirmPassword">
<BaseInput
v-model="forgotForm.confirmPassword"
type="password"
size="large"
placeholder="请再次输入新密码"
v-model="forgotForm.confirmPassword"
type="password"
size="large"
placeholder="请再次输入新密码"
/>
</FormItem>
</Form>
<BaseButton
class="w-full mt-2"
size="large"
:loading="authStore.isLoading"
@click="handleForgotPassword"
class="w-full mt-2"
size="large"
:loading="authStore.isLoading"
@click="handleForgotPassword"
>
重置密码
</BaseButton>
@@ -585,16 +585,16 @@ onBeforeUnmount(() => {
<div v-if="currentMode === 'login'" class="center flex-col bg-gray-100 rounded-xl px-12">
<div class="relative w-40 h-40 bg-white rounded-xl overflow-hidden shadow-xl">
<img
v-if="showWechatQR"
:src="wechatQRUrl"
alt="微信登录二维码"
class="w-full h-full"
:class="{ 'opacity-30': qrStatus === 'expired' }"
v-if="showWechatQR"
:src="wechatQRUrl"
alt="微信登录二维码"
class="w-full h-full"
:class="{ 'opacity-30': qrStatus === 'expired' }"
/>
<!-- 扫描成功蒙层 -->
<div
v-if="qrStatus === 'scanned'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
v-if="qrStatus === 'scanned'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
>
<IconFluentCheckmarkCircle20Filled class="color-green text-4xl"/>
<div class="text-base text-gray-700 font-medium">扫描成功</div>
@@ -602,8 +602,8 @@ onBeforeUnmount(() => {
</div>
<!-- 取消登录蒙层 -->
<div
v-if="qrStatus === 'cancelled'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
v-if="qrStatus === 'cancelled'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
>
<IconFluentErrorCircle20Regular class="color-red text-4xl"/>
<div class="text-base text-gray-700 font-medium">你已取消此次登录</div>
@@ -612,12 +612,12 @@ onBeforeUnmount(() => {
</div>
<!-- 过期蒙层 -->
<div
v-if=" qrStatus === 'expired'"
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
v-if=" qrStatus === 'expired'"
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
>
<IconFluentArrowClockwise20Regular
@click="refreshQRCode"
class="cp text-4xl"/>
@click="refreshQRCode"
class="cp text-4xl"/>
</div>
</div>
<p class="mt-4 center gap-space">

View File

@@ -12,8 +12,6 @@ import BookList from "@/pages/article/BookList.vue";
import Setting from "@/pages/setting/Setting.vue";
import Login from "@/pages/user/login.vue";
import User from "@/pages/user/index.vue";
import UserAgreement from "@/pages/user/UserAgreement.vue";
import PrivacyPolicy from "@/pages/user/PrivacyPolicy.vue";
import { useAuthStore } from "@/stores/auth.ts";
export const routes: RouteRecordRaw[] = [
@@ -38,8 +36,6 @@ export const routes: RouteRecordRaw[] = [
{path: 'setting', component: Setting},
{path: 'login', component: Login},
{path: 'user', component: User},
{path: 'user-agreement', component: UserAgreement},
{path: 'privacy-policy', component: PrivacyPolicy},
]
},
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},

View File

@@ -1,9 +1,17 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login as loginApi, register as registerApi, logout as logoutApi, getUserInfo, resetPassword as resetPasswordApi, type LoginParams, type RegisterParams } from '@/apis/auth'
import {
loginApi,
registerApi,
logoutApi,
getUserInfo,
resetPasswordApi,
type LoginParams,
type RegisterParams
} from '@/apis/user.ts'
import Toast from '@/components/base/toast/Toast.ts'
import router from '@/router.ts'
import {sleep} from "@/utils";
export interface User {
id: string
email?: string
@@ -42,12 +50,10 @@ export const useAuthStore = defineStore('auth', () => {
try {
isLoading.value = true
const response = await loginApi(params)
if (response.success && response.data) {
if (response.success) {
setToken(response.data.token)
setUser(response.data.user)
Toast.success('登录成功')
// 跳转到首页或用户中心
router.push('/')
return true
@@ -73,7 +79,7 @@ export const useAuthStore = defineStore('auth', () => {
} finally {
clearToken()
Toast.success('已退出登录')
router.push('/login')
router.push('/')
}
}
@@ -81,7 +87,7 @@ export const useAuthStore = defineStore('auth', () => {
const fetchUserInfo = async () => {
try {
const response = await getUserInfo()
if (response.success && response.data) {
if (response.success) {
setUser(response.data)
return true
}
@@ -97,12 +103,12 @@ export const useAuthStore = defineStore('auth', () => {
try {
isLoading.value = true
const response = await registerApi(params)
if (response.success && response.data) {
setToken(response.data.token)
setUser(response.data.user)
Toast.success('注册成功')
// 跳转到首页或用户中心
router.push('/')
return true
@@ -124,16 +130,16 @@ export const useAuthStore = defineStore('auth', () => {
try {
isLoading.value = true
const response = await resetPasswordApi(params)
if (response.success) {
Toast.success('密码重置成功')
return { success: true, msg: '密码重置成功' }
return {success: true, msg: '密码重置成功'}
} else {
return { success: false, msg: response.msg || '重置失败' }
return {success: false, msg: response.msg || '重置失败'}
}
} catch (error) {
console.error('Reset password error:', error)
return { success: false, msg: '重置密码失败,请重试' }
return {success: false, msg: '重置密码失败,请重试'}
} finally {
isLoading.value = false
}