Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dd08421b6 | ||
|
|
a45594a4bc | ||
|
|
d0127ca07c | ||
|
|
b342b24aa5 | ||
|
|
76343f5838 | ||
|
|
37aace8a87 | ||
|
|
be3744a0d1 | ||
|
|
babaf53237 | ||
|
|
81019adff4 | ||
|
|
1108e1b321 | ||
|
|
f41aa192fb | ||
|
|
2c175b2324 | ||
|
|
b8be5859b1 | ||
|
|
58b2edc97b | ||
|
|
200a6bf219 | ||
|
|
f70644e497 | ||
|
|
5cd6d4788a | ||
|
|
4810ffee11 | ||
|
|
03fc133850 | ||
|
|
e17c4132bc | ||
|
|
5c51db1dfe | ||
|
|
58b14c385c | ||
|
|
56036d6c00 | ||
|
|
daef6594b6 | ||
|
|
76943141f7 | ||
|
|
7d54be028a | ||
|
|
f5b2bf5adc | ||
|
|
f9994a5ce8 | ||
|
|
8cdf58310e | ||
|
|
75c49e89c2 | ||
|
|
8405471afd | ||
|
|
1e0b38451c | ||
|
|
b84823610c | ||
|
|
332ddbe0e8 | ||
|
|
9cfbf48ce9 | ||
|
|
8f791ff6e5 | ||
|
|
6cb2afca39 | ||
|
|
d47613970b | ||
|
|
8833980008 | ||
|
|
0067e66fc8 | ||
|
|
39c4590c3f | ||
|
|
3196962197 | ||
|
|
1ffaaf5225 | ||
|
|
4ad671023d | ||
|
|
45bd2ddb21 | ||
|
|
267f76d6a2 | ||
|
|
67c09f3113 | ||
|
|
c8da8d5406 | ||
|
|
6ad0a63c68 | ||
|
|
ab4eb00bc1 | ||
|
|
954085666a | ||
|
|
35aa26f8a9 | ||
|
|
1e281508b2 | ||
|
|
a4e4c20c5e | ||
|
|
d4b85f38d8 | ||
|
|
ed88512bf8 | ||
|
|
b9b2f713b9 | ||
|
|
b1563b0d39 | ||
|
|
1857663c9f | ||
|
|
a28922acf0 | ||
|
|
cdacb1643c | ||
|
|
ad0327bf51 | ||
|
|
50af1bf573 | ||
|
|
d364394575 | ||
|
|
98651f4d8f | ||
|
|
40ccb84922 | ||
|
|
d9a19ce3ee | ||
|
|
e216b97163 | ||
|
|
1f51b628af | ||
|
|
1656670203 | ||
|
|
b8dcbdc448 | ||
|
|
dc2e255cd2 | ||
|
|
d9245ed2c9 | ||
|
|
8f4058977d | ||
|
|
044fa0f30b | ||
|
|
6565c50e08 | ||
|
|
184982eb08 | ||
|
|
9a1c53d9f4 | ||
|
|
44678985e9 | ||
|
|
077c8870b4 | ||
|
|
d1fcb6dc11 | ||
|
|
9114d65308 | ||
|
|
f5ec612850 | ||
|
|
a3355e2545 | ||
|
|
1516c1a941 | ||
|
|
b974e8bd3d | ||
|
|
a389ef66bc | ||
|
|
dcdeaa0394 | ||
|
|
f4d7d7a416 | ||
|
|
f5a91c0ece | ||
|
|
5ba6587b6c | ||
|
|
f2de18aa43 | ||
|
|
24cc7e360a | ||
|
|
3c5f6bf873 | ||
|
|
81c9b82abd | ||
|
|
ac3056b9a7 | ||
|
|
10719a4067 | ||
|
|
5597eb651f | ||
|
|
385013db9d | ||
|
|
2699039c0b | ||
|
|
831768dafd | ||
|
|
6ea37823c3 | ||
|
|
67e9f6c98b | ||
|
|
f19f192170 | ||
|
|
7eb4b75ab1 | ||
|
|
d0138516e7 | ||
|
|
ae4f37f1c5 | ||
|
|
41a9d1edf6 | ||
|
|
61fba98d89 | ||
|
|
cc6a35d7c7 | ||
|
|
ea0303835f | ||
|
|
7b993655ba | ||
|
|
d9d7c94513 | ||
|
|
cc6b38acdd | ||
|
|
27d847fd25 | ||
|
|
9e76400c8e | ||
|
|
c365459d88 | ||
|
|
079b2eefb6 | ||
|
|
c6d4c6c1a9 | ||
|
|
9b6355c68d | ||
|
|
ccacee5f95 | ||
|
|
397a51bd96 | ||
|
|
f144945215 | ||
|
|
a1c78172ca | ||
|
|
f03ab1a845 | ||
|
|
a64eca3aeb | ||
|
|
bc547f90f9 | ||
|
|
7a66854a22 | ||
|
|
795a9204ae | ||
|
|
11debaf213 | ||
|
|
857b7dba80 | ||
|
|
5d6bd822d3 | ||
|
|
0b7df2cfc1 | ||
|
|
20bc7ebaaa | ||
|
|
c1a8060139 | ||
|
|
5558dd181b | ||
|
|
0fd4fb283a | ||
|
|
00a7af2d5b | ||
|
|
e34a493b3e | ||
|
|
a9df9ea56a | ||
|
|
d89c0df425 | ||
|
|
d0730e7993 | ||
|
|
5fe376f978 | ||
|
|
dd5b1c55c9 | ||
|
|
7645c6d9fd | ||
|
|
61073970fa | ||
|
|
9ae2ac02eb | ||
|
|
6622c329fc | ||
|
|
b66d545985 | ||
|
|
1789cb5a9b | ||
|
|
ee376c477d | ||
|
|
2b6f0905a9 | ||
|
|
c953754bed | ||
|
|
cea4a10baa | ||
|
|
b708d86eff | ||
|
|
28aab8c7d4 | ||
|
|
9412e3f344 | ||
|
|
b4d5619b1b | ||
|
|
8ce598ad54 | ||
|
|
ca641055e0 | ||
|
|
a5a4099374 | ||
|
|
729dc23a16 | ||
|
|
3e898083ac | ||
|
|
492dd7b052 | ||
|
|
3e5890b8c2 | ||
|
|
e21bde54be | ||
|
|
fdf8e600a0 | ||
|
|
1b5b89e282 | ||
|
|
4cc9037f25 | ||
|
|
e8b3a6c49b | ||
|
|
6bae2d2d0c | ||
|
|
f42a0b530b | ||
|
|
0fdf45c73f | ||
|
|
fd07ad088c | ||
|
|
2a486dc01e | ||
|
|
15686b44ad | ||
|
|
2b474ddb35 | ||
|
|
25e2a1e931 | ||
|
|
59f6e0091f | ||
|
|
fe41a207fa | ||
|
|
1d36c0c56e | ||
|
|
7cca5c88f3 | ||
|
|
00011f8419 | ||
|
|
1cfd85f9de | ||
|
|
3e05f1b8b3 | ||
|
|
a67c629fa6 | ||
|
|
691076bb14 | ||
|
|
1d408e6a09 | ||
|
|
3a3bbcc32a | ||
|
|
a2a0e4ff1c | ||
|
|
1453402ac3 | ||
|
|
7ce127a9e6 | ||
|
|
c9ecfd213c | ||
|
|
1d08ba9905 | ||
|
|
6ffcf4dca7 | ||
|
|
8f851f5e51 | ||
|
|
29b672cdab | ||
|
|
27fe0e04f6 | ||
|
|
591dbe06a2 | ||
|
|
43b7931591 | ||
|
|
51257df07c | ||
|
|
c282f362f4 | ||
|
|
425d6e00eb | ||
|
|
564d470240 | ||
|
|
a1937448d6 | ||
|
|
1ddb1bd5b5 | ||
|
|
4f4ee34e8d | ||
|
|
8202e8c9db | ||
|
|
b797ffb5af | ||
|
|
c2795c49e4 | ||
|
|
545cb6606c | ||
|
|
d0211cfd11 | ||
|
|
a3c9ffdad5 | ||
|
|
5ce1ffd557 | ||
|
|
850d8d7890 | ||
|
|
53cc6eb8ce | ||
|
|
c7af974d3f | ||
|
|
bb143ac15a | ||
|
|
c6c9020d89 | ||
|
|
df1929ab24 | ||
|
|
3d15c37182 | ||
|
|
d9810bfb67 | ||
|
|
5a04889f84 | ||
|
|
8a964bf3f5 | ||
|
|
f573e3e93b | ||
|
|
84a02a42aa | ||
|
|
c750908c04 | ||
|
|
af89388294 | ||
|
|
09380dc94f | ||
|
|
82c84b2afe | ||
|
|
f404aba689 | ||
|
|
9013b1917e | ||
|
|
c415dfa8e1 | ||
|
|
5765e967ae | ||
|
|
1ba4d959ce | ||
|
|
df8ab6bc55 | ||
|
|
ca6668331f | ||
|
|
d49d05b776 | ||
|
|
d8e3f7dca1 | ||
|
|
5ee1a64587 | ||
|
|
9a31c21943 | ||
|
|
e52f0f42cb | ||
|
|
1f51525dfa | ||
|
|
a1c77829cd | ||
|
|
07771c180d | ||
|
|
d55ddbebaf | ||
|
|
bea5d9428e | ||
|
|
e275cbdaee | ||
|
|
5742582073 | ||
|
|
eab51c0355 | ||
|
|
6734df119a | ||
|
|
42269f78c3 | ||
|
|
93d7ecb1ee | ||
|
|
70e1b7a078 | ||
|
|
e359471361 | ||
|
|
7df6616e54 | ||
|
|
2263897111 | ||
|
|
d1b6db4769 | ||
|
|
a001ff8099 | ||
|
|
ee625ea3ed | ||
|
|
3cf31828e0 | ||
|
|
bcf7467308 | ||
|
|
997a3068ac | ||
|
|
753403b0f8 | ||
|
|
1d4fb2cdb2 | ||
|
|
82ce4f00f5 | ||
|
|
068503d275 | ||
|
|
c39375b776 | ||
|
|
4669d6b29b | ||
|
|
e3b422cc6e | ||
|
|
c23a3ccfaf | ||
|
|
5d8b177a12 | ||
|
|
5dd6513706 | ||
|
|
bdb1a67d44 | ||
|
|
7af22e97ad | ||
|
|
fea346413c | ||
|
|
44af38612c | ||
|
|
13cfc60782 | ||
|
|
9b40c05e64 | ||
|
|
ed9ad54b46 | ||
|
|
380693c66d | ||
|
|
91e31cfbf7 | ||
|
|
371fed69df | ||
|
|
f41ee26067 | ||
|
|
749fe85cad | ||
|
|
eb29d616d1 | ||
|
|
eac920fcae | ||
|
|
659163d662 | ||
|
|
ccb9e2a314 | ||
|
|
5891ec1930 | ||
|
|
2c86992cac | ||
|
|
6d0d772749 | ||
|
|
ca86c5ffb7 | ||
|
|
e80a44c43a | ||
|
|
4a7c9367d4 | ||
|
|
bd242c95f7 | ||
|
|
40a8c73461 | ||
|
|
5ae234714f | ||
|
|
449b7581d8 | ||
|
|
d0eafb1e09 | ||
|
|
47d5697653 | ||
|
|
0dfc201990 | ||
|
|
cd260fbcda | ||
|
|
f87ae9a25b | ||
|
|
0ee473fd07 | ||
|
|
36f45b2c26 | ||
|
|
51bed591d4 | ||
|
|
f255595b30 | ||
|
|
bbd4a4cd95 | ||
|
|
95b0cdebfb | ||
|
|
0737adb389 | ||
|
|
9878482654 | ||
|
|
4d31fe7a92 | ||
|
|
55babcee4c | ||
|
|
1bd4bba2f5 | ||
|
|
013ea25731 | ||
|
|
4a17ffbebe | ||
|
|
e3efca9648 | ||
|
|
da54a7e5f5 | ||
|
|
1eff840afa | ||
|
|
2b19022e29 | ||
|
|
2fef16e58b | ||
|
|
09f9dd2784 | ||
|
|
e9c4f295ce | ||
|
|
3684d5a703 | ||
|
|
82c61753e3 | ||
|
|
19f34d6a81 | ||
|
|
d500e22b2f | ||
|
|
568ed2b9d7 | ||
|
|
bfac3ff26e | ||
|
|
ddfe7c8851 | ||
|
|
d12c1c31f3 | ||
|
|
d5cd7d1ae4 | ||
|
|
841fbd1ae1 | ||
|
|
1f0f35b402 | ||
|
|
ffc932951b | ||
|
|
9f09d37936 | ||
|
|
f8c246796d | ||
|
|
68906b7e81 | ||
|
|
fffff50a00 | ||
|
|
b7260a40ad | ||
|
|
a12713f89a | ||
|
|
0fa264a7bc | ||
|
|
5dcef7b408 | ||
|
|
b6f1827fe0 | ||
|
|
57dd4178f4 | ||
|
|
cd131ba7a2 | ||
|
|
fad9f484e8 | ||
|
|
45063936eb | ||
|
|
d1cc297169 | ||
|
|
cebbe90eac | ||
|
|
0ca5aab05a | ||
|
|
a66061aa89 | ||
|
|
d98d5e8c41 | ||
|
|
212f981b6a | ||
|
|
2b3849a573 | ||
|
|
b5d0b5a170 | ||
|
|
66595d10a3 | ||
|
|
2a20042680 | ||
|
|
549948ba37 | ||
|
|
2f3e5171b0 | ||
|
|
cfa68999a9 | ||
|
|
6109c45dbe | ||
|
|
e8a4922541 | ||
|
|
71b4a10991 | ||
|
|
0d2aa3e8b8 | ||
|
|
d7d8dcda1c | ||
|
|
1fa14118ff | ||
|
|
7bb947c14a | ||
|
|
d8011c8738 | ||
|
|
adb6d63eb4 | ||
|
|
f1baf96f95 | ||
|
|
d0e82b56ab | ||
|
|
c64519d8f9 | ||
|
|
77b4eb10b1 | ||
|
|
e705a1c32b | ||
|
|
114cda0b76 | ||
|
|
847445df82 | ||
|
|
2360c7a12d | ||
|
|
581ef15917 | ||
|
|
3180da36da | ||
|
|
ad20a5da70 | ||
|
|
b04debcf75 | ||
|
|
2cf5268f7d | ||
|
|
43f903c6e0 | ||
|
|
d08fe67ab8 | ||
|
|
88f9574aaa | ||
|
|
2387c2e7df | ||
|
|
1dd4d8296b | ||
|
|
b1e3fcdc30 | ||
|
|
6064535c22 | ||
|
|
8b3d04ca06 | ||
|
|
4b13e492fc | ||
|
|
5319327c37 | ||
|
|
3b13237ad6 | ||
|
|
ef39f5aa1e | ||
|
|
1972f1cb6d | ||
|
|
18103e3d38 | ||
|
|
cb96c878bf | ||
|
|
266b3e3b5c | ||
|
|
6ea8ada989 | ||
|
|
901a66bafa | ||
|
|
c9cb8c6542 |
@@ -0,0 +1 @@
|
||||
github: [insistence]
|
||||
@@ -0,0 +1,170 @@
|
||||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
mysql-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /tmp/.docker_cache
|
||||
key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
|
||||
run: |
|
||||
# Save original Dockerfile.my
|
||||
cp ruoyi-fastapi-backend/Dockerfile.my ruoyi-fastapi-backend/Dockerfile.my.bak
|
||||
# Create temporary Dockerfile with target Python version
|
||||
sed "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" ruoyi-fastapi-backend/Dockerfile.my.bak > ruoyi-fastapi-backend/Dockerfile.my
|
||||
|
||||
- name: Start services with docker-compose
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.my.yml up -d --build
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
# Wait for backend to be running
|
||||
timeout 180 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-backend-my | grep -q "Up"; do sleep 5; done'
|
||||
# Wait for frontend to be running
|
||||
timeout 120 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
|
||||
# Additional wait for services to be fully ready and listening on ports
|
||||
sleep 30
|
||||
# Check that backend is actually responding on the API endpoint
|
||||
echo "Checking if backend service is ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service is ready!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for backend service to be ready... ($i/30)"
|
||||
sleep 5
|
||||
done
|
||||
# Final check
|
||||
if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service failed to start properly. Checking logs..."
|
||||
docker logs ruoyi-backend-my-test
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
pip install -r requirements.txt
|
||||
playwright install
|
||||
pytest -v
|
||||
|
||||
- name: Stop services
|
||||
if: always()
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.my.yml down
|
||||
|
||||
- name: Restore original Dockerfile.my
|
||||
if: always()
|
||||
run: |
|
||||
# Restore original Dockerfile.my after test
|
||||
mv ruoyi-fastapi-backend/Dockerfile.my.bak ruoyi-fastapi-backend/Dockerfile.my
|
||||
|
||||
pg-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /tmp/.docker_cache
|
||||
key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
|
||||
run: |
|
||||
# Save original Dockerfile.my
|
||||
cp ruoyi-fastapi-backend/Dockerfile.my ruoyi-fastapi-backend/Dockerfile.my.bak
|
||||
# Create temporary Dockerfile with target Python version
|
||||
sed "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" ruoyi-fastapi-backend/Dockerfile.my.bak > ruoyi-fastapi-backend/Dockerfile.my
|
||||
|
||||
- name: Start services with docker-compose
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.pg.yml up -d --build
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
# Wait for backend to be running
|
||||
timeout 180 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-backend-pg | grep -q "Up"; do sleep 5; done'
|
||||
# Wait for frontend to be running
|
||||
timeout 120 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
|
||||
# Additional wait for services to be fully ready and listening on ports
|
||||
sleep 30
|
||||
# Check that backend is actually responding on the API endpoint
|
||||
echo "Checking if backend service is ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service is ready!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for backend service to be ready... ($i/30)"
|
||||
sleep 5
|
||||
done
|
||||
# Final check
|
||||
if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service failed to start properly. Checking logs..."
|
||||
docker logs ruoyi-backend-pg-test
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
pip install -r requirements.txt
|
||||
playwright install
|
||||
pytest -v
|
||||
|
||||
- name: Stop services
|
||||
if: always()
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.pg.yml down
|
||||
|
||||
- name: Restore original Dockerfile.my
|
||||
if: always()
|
||||
run: |
|
||||
# Restore original Dockerfile.my after test
|
||||
mv ruoyi-fastapi-backend/Dockerfile.my.bak ruoyi-fastapi-backend/Dockerfile.my
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Ruff Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install ruff
|
||||
|
||||
- name: Run linting
|
||||
run: |
|
||||
ruff check ruoyi-fastapi-backend
|
||||
ruff check ruoyi-fastapi-test
|
||||
|
||||
- name: Run format check
|
||||
run: |
|
||||
ruff format ruoyi-fastapi-backend --check
|
||||
ruff format ruoyi-fastapi-test --check
|
||||
@@ -18,6 +18,7 @@ lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
vf_admin/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
@@ -135,3 +136,19 @@ dmypy.json
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# AI Editor
|
||||
.agent/
|
||||
.claude/
|
||||
.codebuddy/
|
||||
.codex/
|
||||
.cursor/
|
||||
.opencode/
|
||||
.qoder/
|
||||
.trae/
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
# 更新日志
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.9.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
前后端依赖均有升级,请升级依赖或重新创建环境。
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增AI管理模块 ([#69](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/69))。
|
||||
2.新增移动端模块 ([#73](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/73))。
|
||||
3.新增多worker运行支持 ([#76](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/76))。
|
||||
4.应用新增演示模式 ([#78](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/78))。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复代码生成controller模板删除接口query_db参数异常的问题 ([#63](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/63))。
|
||||
2.修复登录接口response_model声明错误 ([#71](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/71))。
|
||||
3.修复无法直接通过后端地址访问API文档的问题 ([#74](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/74))。
|
||||
4.修复create_app重复执行的问题 ([#84](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/84))。
|
||||
|
||||
### 代码重构
|
||||
|
||||
1.移除对python3.9的支持 ([#67](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/67))。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化alembic处理表模型逻辑,避免无关表影响 ([#68](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/68))。
|
||||
2.优化代码生成后端模板 ([#72](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/72))。
|
||||
3.自动注册路由出错时抛出异常以便于调试 ([#79](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/79))。
|
||||
4.优化部分页面字段tooltip说明 (#80)。
|
||||
5.优化项目启动速度 ([#82](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/82))。
|
||||
6.优化暗黑模式切换效果 ([#83](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/83))。
|
||||
7.优化热重载模式或单worker下scheduler的任务状态同步机制 ([#85](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/85))。
|
||||
8.优化防重提交间隔时间可自定义 ([#87](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/87))。
|
||||
9.优化验证码计算结果为非负数 ([#88](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/88))。
|
||||
10.优化ci测试稳定性 ([#90](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/90))。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.8.1
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增E2E测试 ([#57](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/57))。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复DictTag组件渲染异常的问题 ([#59](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/59))。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化数据权限依赖 ([#55](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/55))。
|
||||
2.动态导入定时任务函数,移除eval ([#56](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/56))。
|
||||
3.优化pg版本的docker compose配置文件 ([#61](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/61))。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.8.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.后端依赖升级到最新版本,请升级依赖或重新创建环境。
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增请求上下文管理类。
|
||||
2.新增`PreAuthDependency`、`CurrentUserDependency`、`DataScopeDependency`、`DBSessionDependency`、`UserInterfaceAuthDependency`和`RoleInterfaceAuthDependency`依赖函数。
|
||||
3.新增上下文清理中间件。
|
||||
4.新增公共vo模块。
|
||||
5.新增配置文档静态资源方法。
|
||||
6.新增自动注册路由功能。
|
||||
7.新增docker compose部署方式。
|
||||
8.菜单导航设置支持纯顶部。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复单账号登录模式下强退功能失效的问题 [#52](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/52)。
|
||||
2.确保ApschedulerJobs字段类型与apscheduler默认创建的表字段类型一致 [#53](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/53)。
|
||||
3.修复磁盘存在异常时服务监控无法正常运行的问题。
|
||||
4.移除代码生成表业务表外键,修复无法删除的问题。
|
||||
5.修复固定头部时出现的导航栏偏移问题。
|
||||
6.修复表单构建移除所有控件后切换路由回来空白问题。
|
||||
7.修复代码生成v3模板时间控件between选择后清空报错问题。
|
||||
|
||||
### 代码重构
|
||||
|
||||
1.增强ruff规则,完善类型提示。
|
||||
2.优化项目结构,新增common模块,原annotation、aspect、constant、enums模块移动至common模块下。
|
||||
3.重构app与server设计。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.controller层全部使用新依赖项。
|
||||
2.当前用户信息使用上下文变量。
|
||||
3.分页模型改为使用公共vo模块的PageModel。
|
||||
4.优化API文档的响应模型显示。
|
||||
5.操作响应模型改为使用公共vo模块的CrudResponseModel。
|
||||
6.优化API文档的接口描述信息。
|
||||
7.登录/注册页面底部版权信息修改为读取配置。
|
||||
8.优化生成代码下载的zip文件名。
|
||||
9.优化表单构建关闭页签销毁复制插件。
|
||||
10.行内表单默认设置固定宽度。
|
||||
11.优化操作日志详细请求参数显示。
|
||||
12.优化index页面标题读取配置。
|
||||
13.优化字典组件数字类型值处理逻辑。
|
||||
14.优化字典组件值宽松匹配。
|
||||
15.默认固定头部。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.7.1
|
||||
|
||||
### 项目依赖
|
||||
|
||||
1.后端依赖移除passlib,直接使用bcrypt。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复代码生成controller模板编辑接口异常生成字段的问题。
|
||||
2.移除passlib直接使用bcrypt修复密码校验异常的问题 [#48](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/48) [#49](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/49)。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.代码生成do模板补充表描述。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.7.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
1.前后端依赖升级,请升级依赖或重新创建环境。
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增alembic支持。
|
||||
2.文件&图片上传组件支持自定义地址&参数。
|
||||
3.新增默认打包配置项。
|
||||
4.显隐列组件支持全选/全不选。
|
||||
5.添加页签openPage支持传递参数。
|
||||
6.外链加载时遮罩信息提示。
|
||||
7.上传组件新增拖动排序属性。
|
||||
8.图片上传组件新增disabled属性。
|
||||
9.代码生成列支持拖动排序。
|
||||
10.新增用户默认初始化密码。
|
||||
11.新增页签图标显示开关功能。
|
||||
12.新增底部版权信息及开关。
|
||||
13.用户归属部门新增清除。
|
||||
14.用户导入新增验证提示。
|
||||
15.菜单搜索支持键盘选择&悬浮主题背景。
|
||||
16.新增apscheduler_jobs表对应sqlalchemy模型类。
|
||||
17.初始密码支持自定义修改策略。
|
||||
18.账号密码支持自定义更新周期。
|
||||
19.注册账号设置默认密码最后更新时间。
|
||||
20.显示列信息支持对象格式。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复logout接口未按照app_same_time_login配置项动态判断的问题 [#IBZZ1S](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IBZZ1S)。
|
||||
2.修复上传组件被多次引用拖动仅对第一个有效的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化接口耗时计算。
|
||||
2.优化启动信息显示。
|
||||
3.优化前端处理路由函数代码。
|
||||
4.登录页和注册页表头使用VITE_APP_BASE_API配置值。
|
||||
5.优化角色禁用不允许分配。
|
||||
6.优化富文本控制台警告异常。
|
||||
7.优化checkbox废弃API。
|
||||
8.优化导航栏显示昵称&设置。
|
||||
|
||||
### 代码重构
|
||||
|
||||
1.重构IP归属区域查询为异步调用。
|
||||
2.调整do与sql使其相互适配以支持alembic。
|
||||
3.富文本复制粘贴图片上传至url。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.6.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.文件上传组件新增disabled属性。
|
||||
2.文件上传组件新增类型。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复日志管理时间查询报错 [#27](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/27)。
|
||||
2.修复定时任务状态暂停时执行单次任务会触发cron表达式的问题 [#31](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/31)。
|
||||
3.修复修改字典类型时获取dict_code异常的问题。
|
||||
4.修复修改字典类型时字典数据更新时间异常的问题。
|
||||
5.修复代码生成模板时间查询问题 [#28](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/28)。
|
||||
6.修复用户导出缺失部门名称的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化代码生成新增和编辑字段显示和渲染。
|
||||
2.pagination更换成flex布局。
|
||||
3.优化代码生成vue模板 [#23](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/23)。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.6.1
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.新增sqlglot依赖
|
||||
|
||||
```bash
|
||||
pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.引入sqlglot修复sql语句解析异常的问题。
|
||||
2.修复代码生成字段唯一性校验dao层模板判断异常的问题。
|
||||
3.引入泛型修复as_query和as_form装饰模型文档丢失的问题。
|
||||
4.修复代码生成主子表vo模板可能缺失NotBlank的问题。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.6.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
1.后端依赖升级到最新版本,请升级依赖或重新创建环境。
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增代码生成功能,支持配置数据库表信息一键生成和下载前后端代码,需要重新执行sql文件,请先备份数据。
|
||||
2.新增表单构建功能。
|
||||
3.用户头像新增支持http(s)链接。
|
||||
4.新增trace中间件强化日志链路追踪和响应头 [@y1ren](https://gitee.com/y1ren)。
|
||||
5.用户管理支持分栏拖动。
|
||||
6.菜单面包屑导航支持多层级显示。
|
||||
7.白名单支持对通配符路径匹配。
|
||||
8.支持开启暗黑模式。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复默认关闭Tags-Views时,内链页面打不开。
|
||||
2.修复删除当前登录用户拦截失效的问题。
|
||||
3.修复定时任务目标字符串规则校验不全的问题。
|
||||
4.修复执行单次任务时会覆盖已启用任务的问题 [#IBEKD2](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IBEKD2)。
|
||||
5.修复个人中心特殊字符密码修改失败问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化导出方法。
|
||||
2.参数键值更换为多行文本。
|
||||
3.优化日志中操作方法显示。
|
||||
4.优化日志装饰器获取核心参数的方式。
|
||||
5.用户管理过滤掉已禁用部门。
|
||||
6.优化TopNav内链菜单点击没有高亮。
|
||||
7.ResponseUtil补充完整参数。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.5.1
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.定时任务新增支持调用异步函数。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化字典数组条件判断。
|
||||
2.校检文件名是否包含特殊字符。
|
||||
3.移除已弃用的log_decorator装饰器。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.5.0
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增对PostgreSQL数据库的支持。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复DictTag组件控制台抛异常的问题 [#IAYSVZ](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IAYSVZ)。
|
||||
2.修复登录日志导出文件名称错误的问题。
|
||||
|
||||
### 代码回滚
|
||||
|
||||
1.因fastapi查询参数模型底层存在bug,回滚查询参数模型声明方式为as_query。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化CamelCaseUtil和SnakeCaseUtil以兼容更多转换场景。
|
||||
2.优化列表查询排序。
|
||||
3.优化参数设置页面。
|
||||
4.优化上传图片带域名时不增加前缀。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.4.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.更新fastapi版本为0.115.0
|
||||
|
||||
```bash
|
||||
pip install fastapi[all]==0.115.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
### 代码重构
|
||||
|
||||
1.基于fastapi 0.115.0版本新特性,直接使用pydantic模型接收查询参数和表单数据,移除原有as_query和as_form使用方式。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复角色管理service书写错误。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化前端登录请求方法。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.3.3
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.更新pydantic-validation-decorator版本为0.1.4,修复了一些底层bug。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复在线用户模块条件查询无效的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化在线用户模块前后端字段描述一致。
|
||||
2.日志装饰器异常处理增加logger打印日志。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.3.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增gzip压缩中间件。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复分页函数计算has_next错误的问题 [#10](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/10)。
|
||||
2.修复定时任务监听函数中事件没有job_id报错的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化添加中间件函数注释。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.3.1
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复1.3.0版本采用新的异常处理机制后日志装饰器无法记录异常日志的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.补充定时任务违规字符串。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.3.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
1.前后端依赖均升级到最新版本,请升级依赖或重新创建环境。
|
||||
2.使用`PyJWT`替换`python-jose`以解决一些安全性问题。
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增字段校验装饰器,支持手动触发校验,已封装为`pydantic-validation-decorator`库。
|
||||
2.各模块`service`层新增字段唯一性校验。
|
||||
3.全局新增`ServiceException`自定义服务异常和`ServiceWarning`自定义服务警告,无需在接口中写大量的异常捕获。
|
||||
4.菜单管理新增路由名称,请执行以下sql为数据库新增字段:
|
||||
|
||||
```sql
|
||||
ALTER TABLE sys_menu ADD COLUMN route_name varchar(50) DEFAULT '';
|
||||
```
|
||||
|
||||
5.新增`constant`常量配置及`enums`枚举类型配置。
|
||||
6.新增`StringUtil`、`CronUtil`工具类。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复用户管理、角色管理、部门管理越权漏洞。
|
||||
2.修复各模块`dao`层`status`、`del_flag`类型与数据库不一致的问题。
|
||||
3.修复移动端左侧菜单无法显示的问题。
|
||||
4.修复其他已知BUG。
|
||||
|
||||
### 代码重构
|
||||
|
||||
1.重构日志装饰器为`Log`,未来版本将删除`log_decorator`装饰器,请尽快迁移。
|
||||
2.重构`RedisInitKeyConfig`为枚举类型,现在可通过以下方式获取对应的`key`和`remark`
|
||||
`RedisInitKeyConfig.ACCESS_TOKEN.key`、`RedisInitKeyConfig.ACCESS_TOKEN.remark`。
|
||||
3.重构数据权限逻辑,底层进行优化,使用方法与之前相同。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.引入`ruff`对后端代码进行格式化及检测修复,优化导入。
|
||||
2.各模块基于`ServiceException`自定义服务异常和`ServiceWarning`自定义服务警告优化了异常处理逻辑。
|
||||
3.各模块`vo`层使用`Field`声明字段。
|
||||
4.优化API文档字段描述显示。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.2.2
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复删除定时任务时未移除调度中任务的问题。
|
||||
2.修复菜单生成路由时组件条件判断错误的问题。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.2.1
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复各模块新增数据时创建时间记录异常的问题。
|
||||
2.修复菜单挂载到根目录时路由加载异常等一系列相关问题。
|
||||
|
||||
### 代码及性能优化
|
||||
|
||||
1.修改代理localhost为127.0.0.1以适配部分设备解析localhost异常的问题。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.2.0
|
||||
|
||||
### 重要说明
|
||||
|
||||
本次更新为 **_破坏性更新_** ,重构数据库orm为异步,代码改动很大,请谨慎升级。
|
||||
1.原有的Session类型声明统一变更为AsyncSession。
|
||||
2.service层和dao层的函数修改为异步函数,请使用await调用。
|
||||
3.orm查询不再支持query,请使用select、update、delete等语句,具体使用方法请参考[https://docs.sqlalchemy.org/en/20/orm/queryguide/index.html](https://docs.sqlalchemy.org/en/20/orm/queryguide/index.html)。
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.增加asyncmy依赖用于支持orm异步操作mysql,请重新安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
|
||||
```
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增SnakeCaseUtil工具类,将原CamelCaseUtil工具类的camel_to_snake函数迁移至SnakeCaseUtil工具类。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复用户管理模块重置用户密码时会异常重置用户岗位和角色的问题。
|
||||
2.修复清空定时任务日志异常的问题。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.1.3
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.用户密码新增非法字符验证。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复通知公告列表查询前后端字段不一致的问题。
|
||||
2.修复个人中心修改基本资料后端异常的问题。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.1.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.配置文件新增数据库连接池相关配置。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复个人中心修改密码后端异常的问题。
|
||||
|
||||
### 代码及性能优化
|
||||
|
||||
1.使用@lru_cache缓存ip归属区域查询结果,避免重复调用ip归属区域查询接口以优化性能。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.1.1
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复编辑定时任务时更新的信息未同步至scheduler的问题 [#I9EK56](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9EK56)。
|
||||
2.修复编辑角色数据权限时后端异常的问题 [#I9ENQN](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9ENQN)。
|
||||
3.修复菜单配置路由参数不生效的问题。
|
||||
4.修复获取路由信息时菜单排序不生效的问题。
|
||||
5.修复添加菜单时是否外链和是否缓存回显异常的问题。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.1.0
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.后端配置文件新增sqlalchemy日志开关配置。
|
||||
2.后端配置文件新增IP归属区域查询开关配置。
|
||||
3.后端配置文件新增账号同时登录开关配置。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复token本身过期时退出登录接口异常的问题 [#I9CBWT](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9CBWT)。
|
||||
2.修复系统版本号或浏览器版本号无法获取时登录异常的问题 [#I9CYNM](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9CYNM)。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.0.3
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.账号密码登录新增IP黑名单校验。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复外链菜单无法打开的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I95KBK)。
|
||||
2.修复添加和编辑菜单页面中是否缓存和是否外链字段回显异常的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I95KBK)。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.0.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.用户接口权限校验增加列表接收参数,实现同一接口支持多个权限标识校验。
|
||||
2.新增按角色校验接口权限依赖
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复用户管理和部门管理模块数据权限异常的问题。
|
||||
|
||||
### 代码及性能优化
|
||||
|
||||
1.调整参数设置、部门管理、字典管理、定时任务、日志管理、角色管理、菜单管理模块部分接口权限标识。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.0.1
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.更新fastapi版本为0.109.1,修复一些安全性问题,命令:
|
||||
|
||||
```bash
|
||||
pip install fastapi[all]==0.109.1 -i https://mirrors.aliyun.com/pypi/simple/
|
||||
```
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.日志管理模块新增字段排序查询。
|
||||
|
||||
## RuoYi-Vue3-FastAPI v1.0.0
|
||||
|
||||
RuoYi-Vue3-FastAPI第一个版本发布啦!
|
||||
此版本功能如下:
|
||||
1.用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2.角色管理:角色菜单权限分配。
|
||||
3.菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
4.部门管理:配置系统组织机构(公司、部门、小组)。
|
||||
5.岗位管理:配置系统用户所属担任职务。
|
||||
6.字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7.参数管理:对系统动态配置常用参数。
|
||||
8.通知公告:系统通知公告信息发布维护。
|
||||
9.操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10.登录日志:系统登录日志记录查询包含登录异常。
|
||||
11.在线用户:当前系统中活跃用户状态监控。
|
||||
12.定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13.服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
14.缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
15.系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
@@ -1,105 +1,191 @@
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.1.2</h1>
|
||||
<h1 align="center">
|
||||
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
|
||||
</h1>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI</h1>
|
||||
<h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4>
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers"><img src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark"></a>
|
||||
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social"></a>
|
||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.1.2-brightgreen.svg"></a>
|
||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
|
||||
<img src="https://img.shields.io/badge/python-≥3.8-blue">
|
||||
<img src="https://img.shields.io/badge/MySQL-≥5.7-blue">
|
||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers">
|
||||
<img alt="Gitee" src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark">
|
||||
</a>
|
||||
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI">
|
||||
<img alt="Github" src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social">
|
||||
</a>
|
||||
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22%22Playwright+Tests%22%22">
|
||||
<img alt="Playwright Tests" src="https://github.com/insistence/RuoYi-Vue3-FastAPI/workflows/Playwright Tests/badge.svg">
|
||||
</a>
|
||||
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22%22Ruff+Check%22%22">
|
||||
<img alt="Ruff Check" src="https://github.com/insistence/RuoYi-Vue3-FastAPI/workflows/Ruff Check/badge.svg">
|
||||
</a>
|
||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI">
|
||||
<img alt="project version" src="https://img.shields.io/badge/version-1.9.0-brightgreen.svg">
|
||||
</a>
|
||||
<a href="https://github.com/astral-sh/ruff">
|
||||
<img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json">
|
||||
</a>
|
||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE">
|
||||
<img alt="LICENSE" src="https://img.shields.io/github/license/mashape/apistatus.svg">
|
||||
</a>
|
||||
<img alt="node version" src="https://img.shields.io/badge/node-≥18-blue">
|
||||
<img alt="python version" src="https://img.shields.io/badge/python-≥3.10-blue">
|
||||
<img alt="mysql version" src="https://img.shields.io/badge/MySQL-≥5.7-blue">
|
||||
<img alt="redis version" src="https://img.shields.io/badge/redis-≥6.2-blue">
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 平台简介
|
||||
|
||||
RuoYi-Vue3-FastAPI是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
|
||||
|
||||
* 前端采用Vue、Element Plus,基于<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>前端项目修改。
|
||||
* 后端采用FastAPI、sqlalchemy、MySQL、Redis、OAuth2 & Jwt。
|
||||
* 前端采用Vue3、Element Plus,基于<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>前端项目修改。
|
||||
* 移动端采用uni-app、Vue3、Vite,内置tailwindcss,基于<u>[RuoYi-App](https://github.com/yangzongzhuan/RuoYi-App)</u>项目修改。
|
||||
* 后端采用FastAPI、sqlalchemy、MySQL(PostgreSQL)、Redis、OAuth2 & Jwt。
|
||||
* 权限认证使用OAuth2 & Jwt,支持多终端认证系统。
|
||||
* 支持加载动态权限菜单,多方式轻松权限控制。
|
||||
* Vue2版本:
|
||||
- Gitte仓库地址:https://gitee.com/insistence2022/RuoYi-Vue-FastAPI
|
||||
- GitHub仓库地址:https://github.com/insistence/RuoYi-Vue-FastAPI
|
||||
* Gitte仓库地址:<https://gitee.com/insistence2022/RuoYi-Vue-FastAPI>
|
||||
* GitHub仓库地址:<https://github.com/insistence/RuoYi-Vue-FastAPI>
|
||||
* 纯Python版本:
|
||||
- Gitte仓库地址:https://gitee.com/insistence2022/dash-fastapi-admin
|
||||
- GitHub仓库地址:https://github.com/insistence/Dash-FastAPI-Admin
|
||||
* 特别鸣谢:<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>
|
||||
* Gitte仓库地址:<https://gitee.com/insistence2022/dash-fastapi-admin>
|
||||
* GitHub仓库地址:<https://github.com/insistence/Dash-FastAPI-Admin>
|
||||
* 特别鸣谢:<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>、<u>[RuoYi-App](https://github.com/yangzongzhuan/RuoYi-App)</u>
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||
3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
4. 部门管理:配置系统组织机构(公司、部门、小组)。
|
||||
5. 岗位管理:配置系统用户所属担任职务。
|
||||
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7. 参数管理:对系统动态配置常用参数。
|
||||
8. 通知公告:系统通知公告信息发布维护。
|
||||
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||
3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
4. 部门管理:配置系统组织机构(公司、部门、小组)。
|
||||
5. 岗位管理:配置系统用户所属担任职务。
|
||||
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7. 参数管理:对系统动态配置常用参数。
|
||||
8. 通知公告:系统通知公告信息发布维护。
|
||||
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10. 登录日志:系统登录日志记录查询包含登录异常。
|
||||
11. 在线用户:当前系统中活跃用户状态监控。
|
||||
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
14. 缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
15. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
15. 在线构建器:拖动表单元素生成相应的HTML代码。
|
||||
16. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
17. 代码生成:配置数据库表信息一键生成前后端代码(python、sql、vue、js),支持下载。
|
||||
18. AI管理:提供AI模型管理和AI对话功能。
|
||||
|
||||
## 演示图
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/login.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dashboard.png"/></td>
|
||||
<td>
|
||||
<img alt="login" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/login.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="dashboard" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/dashboard.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/user.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/role.png"/></td>
|
||||
<td>
|
||||
<img alt="user" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/user.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="role" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/role.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/menu.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dept.png"/></td>
|
||||
<td>
|
||||
<img alt="menu" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/menu.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="dept" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/dept.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/post.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dict.png"/></td>
|
||||
<td>
|
||||
<img alt=""post src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/post.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="dict" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/dict.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/config.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/notice.png"/></td>
|
||||
<td>
|
||||
<img alt="config" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/config.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="notice" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/notice.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/operLog.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/loginLog.png"/></td>
|
||||
<td>
|
||||
<img alt="operLog" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/operLog.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="loginLog" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/loginLog.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/online.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/job.png"/></td>
|
||||
<td>
|
||||
<img alt="online" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/online.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="job" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/job.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/server.png"/></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/cache.png"/></td>
|
||||
<td>
|
||||
<img alt="server" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/server.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="cache" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/cache.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/cacheList.png"></td>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/api.png"></td>
|
||||
<td>
|
||||
<img alt="cacheList" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/cacheList.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="form" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/form.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/profile.png"/></td>
|
||||
<td>
|
||||
<img alt="api" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/api.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="gen" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/gen.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img alt="aiModel" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/aiModel.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="aiChat" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/aiChat.png">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img alt="profile" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/profile.png">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img alt="applogin" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/app/login.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="appWorkbench" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/app/workbench.png">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="appProfile" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/app/profile.png">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 在线体验
|
||||
- *账号:admin*
|
||||
- *密码:admin123*
|
||||
- 演示地址:<a href="https://vfadmin.insistence.tech">vfadmin管理系统<a>
|
||||
|
||||
* *账号:admin*
|
||||
* *密码:admin123*
|
||||
* 演示地址:<a href="https://vfadmin.insistence.tech">vfadmin管理系统<a>
|
||||
|
||||
## 项目开发及发布相关
|
||||
|
||||
@@ -114,6 +200,7 @@ cd RuoYi-Vue3-FastAPI
|
||||
```
|
||||
|
||||
#### 前端
|
||||
|
||||
```bash
|
||||
# 进入前端目录
|
||||
cd ruoyi-fastapi-frontend
|
||||
@@ -128,26 +215,49 @@ npm install --registry=https://registry.npmmirror.com
|
||||
npm run dev 或 yarn dev
|
||||
```
|
||||
|
||||
#### 移动端
|
||||
|
||||
```bash
|
||||
# 进入移动端目录
|
||||
cd ruoyi-fastapi-app
|
||||
|
||||
# 安装依赖
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
|
||||
# 启动 H5
|
||||
pnpm dev:h5
|
||||
|
||||
# 启动微信小程序
|
||||
pnpm dev:mp-weixin
|
||||
```
|
||||
|
||||
移动端详细文档请参考:[ruoyi-fastapi-app/README.md](./ruoyi-fastapi-app/README.md)
|
||||
|
||||
#### 后端
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd ruoyi-fastapi-backend
|
||||
|
||||
# 安装项目依赖环境
|
||||
# 如果使用的是MySQL数据库,请执行以下命令安装项目依赖环境
|
||||
pip3 install -r requirements.txt
|
||||
# 如果使用的是PostgreSQL数据库,请执行以下命令安装项目依赖环境
|
||||
pip3 install -r requirements-pg.txt
|
||||
|
||||
# 配置环境
|
||||
在.env.dev文件中配置开发环境的数据库和redis
|
||||
|
||||
# 运行sql文件
|
||||
1.新建数据库ruoyi-fastapi(默认,可修改)
|
||||
2.使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi.sql
|
||||
2.如果使用的是MySQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi.sql;如果使用的是PostgreSQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi-pg.sql
|
||||
|
||||
# 运行后端
|
||||
python3 app.py --env=dev
|
||||
```
|
||||
|
||||
#### 访问
|
||||
|
||||
```bash
|
||||
# 默认账号密码
|
||||
账号:admin
|
||||
@@ -160,6 +270,7 @@ python3 app.py --env=dev
|
||||
### 发布
|
||||
|
||||
#### 前端
|
||||
|
||||
```bash
|
||||
# 构建测试环境
|
||||
npm run build:stage 或 yarn build:stage
|
||||
@@ -169,6 +280,7 @@ npm run build:prod 或 yarn build:prod
|
||||
```
|
||||
|
||||
#### 后端
|
||||
|
||||
```bash
|
||||
# 配置环境
|
||||
在.env.prod文件中配置生产环境的数据库和redis
|
||||
@@ -177,14 +289,37 @@ npm run build:prod 或 yarn build:prod
|
||||
python3 app.py --env=prod
|
||||
```
|
||||
|
||||
### Docker Compose部署方式
|
||||
|
||||
> ⚠️ **警告:** 默认未做数据持久化配置,请注意数据备份或自行配置持久化
|
||||
|
||||
#### MySQL版本
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.my.yml up -d --build
|
||||
```
|
||||
|
||||
#### PostgreSQL版本
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.pg.yml up -d --build
|
||||
```
|
||||
|
||||
## 交流与赞助
|
||||
|
||||
如果有对本项目及FastAPI感兴趣的朋友,欢迎加入知识星球一起交流学习,让我们一起变得更强。如果你觉得这个项目帮助到了你,你可以请作者喝杯咖啡表示鼓励☕。扫描下面微信二维码添加微信备注VF-Admin即可进群。
|
||||
<table>
|
||||
<tr>
|
||||
<td><img alt="zsxq" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/zsxq.jpg"></td>
|
||||
<td><img alt="zanzhu" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/zanzhu.jpg"></td>
|
||||
<td>
|
||||
<img alt="zsxq" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/common/zsxq.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img alt="zanzhu" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/common/zanzhu.jpg">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img alt="wxcode" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/wxcode.jpg"></td>
|
||||
<td>
|
||||
<img alt="wxcode" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/common/wxcode.jpg">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,73 @@
|
||||
services:
|
||||
# 前端服务
|
||||
ruoyi-frontend:
|
||||
build:
|
||||
context: ./ruoyi-fastapi-frontend
|
||||
dockerfile: Dockerfile
|
||||
image: ruoyi-frontend:latest
|
||||
container_name: ruoyi-frontend
|
||||
ports:
|
||||
- "12580:80"
|
||||
volumes:
|
||||
- ./ruoyi-fastapi-frontend/bin/nginx.dockermy.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- ruoyi-backend-my
|
||||
networks:
|
||||
- ruoyi-network
|
||||
|
||||
# 后端服务(MySQL版本)
|
||||
ruoyi-backend-my:
|
||||
build:
|
||||
context: ./ruoyi-fastapi-backend
|
||||
dockerfile: Dockerfile.my
|
||||
image: ruoyi-backend-my:latest
|
||||
container_name: ruoyi-backend-my
|
||||
ports:
|
||||
- "19099:9099"
|
||||
depends_on:
|
||||
ruoyi-mysql:
|
||||
condition: service_healthy
|
||||
ruoyi-redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- ruoyi-network
|
||||
|
||||
# MySQL服务
|
||||
ruoyi-mysql:
|
||||
image: mysql:8.0
|
||||
container_name: ruoyi-mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: ruoyi-fastapi
|
||||
ports:
|
||||
- "13306:3306"
|
||||
volumes:
|
||||
- ./ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql:/docker-entrypoint-initdb.d/ruoyi-fastapi.sql
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --skip-character-set-client-handshake=1
|
||||
networks:
|
||||
- ruoyi-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# Redis服务
|
||||
ruoyi-redis:
|
||||
image: redis:latest
|
||||
container_name: ruoyi-redis
|
||||
ports:
|
||||
- "16379:6379"
|
||||
networks:
|
||||
- ruoyi-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# 网络配置
|
||||
networks:
|
||||
ruoyi-network:
|
||||
name: ruoyi-network
|
||||
driver: bridge
|
||||
@@ -0,0 +1,73 @@
|
||||
services:
|
||||
# 前端服务
|
||||
ruoyi-frontend:
|
||||
build:
|
||||
context: ./ruoyi-fastapi-frontend
|
||||
dockerfile: Dockerfile
|
||||
image: ruoyi-frontend:latest
|
||||
container_name: ruoyi-frontend
|
||||
ports:
|
||||
- "12580:80"
|
||||
volumes:
|
||||
- ./ruoyi-fastapi-frontend/bin/nginx.dockerpg.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- ruoyi-backend-pg
|
||||
networks:
|
||||
- ruoyi-network
|
||||
|
||||
# 后端服务(PostgreSQL版本)
|
||||
ruoyi-backend-pg:
|
||||
build:
|
||||
context: ./ruoyi-fastapi-backend
|
||||
dockerfile: Dockerfile.pg
|
||||
image: ruoyi-backend-pg:latest
|
||||
container_name: ruoyi-backend-pg
|
||||
ports:
|
||||
- "19099:9099"
|
||||
depends_on:
|
||||
ruoyi-pg:
|
||||
condition: service_healthy
|
||||
ruoyi-redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- ruoyi-network
|
||||
|
||||
# PostgreSQL服务
|
||||
ruoyi-pg:
|
||||
image: postgres:14
|
||||
container_name: ruoyi-pg
|
||||
environment:
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: ruoyi-fastapi
|
||||
POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate=C --lc-ctype=C
|
||||
ports:
|
||||
- "15432:5432"
|
||||
volumes:
|
||||
- ./ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql:/docker-entrypoint-initdb.d/ruoyi-fastapi-pg.sql
|
||||
networks:
|
||||
- ruoyi-network
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# Redis服务
|
||||
ruoyi-redis:
|
||||
image: redis:latest
|
||||
container_name: ruoyi-redis
|
||||
ports:
|
||||
- "16379:6379"
|
||||
networks:
|
||||
- ruoyi-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# 网络配置
|
||||
networks:
|
||||
ruoyi-network:
|
||||
name: ruoyi-network
|
||||
driver: bridge
|
||||
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
@@ -0,0 +1,27 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
unpackage/
|
||||
dist/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.project
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
# 要提交 .env 来确保 jit v2 的开发 watch mode
|
||||
!.env
|
||||
|
||||
src/ignore
|
||||
pnpm-lock.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
# 设置国内镜像地址
|
||||
registry=http://registry.npmmirror.com/
|
||||
# registry=http://registry.npmjs.com/
|
||||
# 这个是给 pnpm 用的
|
||||
shamefully-hoist=true
|
||||
|
||||
ignore-engines=true
|
||||
@@ -0,0 +1,78 @@
|
||||
# RuoYi-FastAPI-App
|
||||
|
||||
基于 `uni-app` 的 `vite` + `vue3` + `tailwindcss` 开发。
|
||||
|
||||
## 特性
|
||||
|
||||
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/) - 快 & 稳定
|
||||
|
||||
- 🎨 [TailwindCSS](https://tailwindcss.com/) - 世界上最流行,生态最好的原子化CSS框架
|
||||
|
||||
- 😃 [集成 Iconify](https://github.com/egoist/tailwindcss-icons) - [icones.js.org](https://icones.js.org/) 中的所有图标都为你所用
|
||||
|
||||
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
|
||||
|
||||
- 🧬 [uni-app 条件编译样式](https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro) - 帮助你在多端更灵活的使用 `TailwindCSS`
|
||||
|
||||
- 🦾 [TypeScript](https://www.typescriptlang.org/) & [ESLint](https://eslint.org/) & [Stylelint](https://stylelint.io/) - 样式,类型,统一的校验与格式化规则,保证你的代码风格和质量
|
||||
|
||||
## 快速开始
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 推荐使用 `"node": "^20.19.0 || >=22.12.0"` 的 Node.js 版本进行开发!
|
||||
>
|
||||
> 另外谨慎升级 `package.json` 中锁定的 `pinia`/`vue`/`@vue/*` 相关包的版本,新版本可能 `uni-app` 没有兼容,造成一些奇怪的 bug
|
||||
|
||||
### vscode
|
||||
|
||||
使用 `vscode` 的开发者,请先安装 [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 智能提示与感应插件
|
||||
|
||||
其他 IDE 请参考: <https://tw.icebreaker.top/docs/quick-start/intelliSense>
|
||||
|
||||
### 更换 Appid
|
||||
|
||||
把 `src/manifest.json` 中的 `appid`, 更换为你自己的 `appid`, 比如 `uni-app` / `mp-weixin` 平台。
|
||||
|
||||
## 升级依赖
|
||||
|
||||
- `pnpm up:pkg` 升级除了 `uni-app` 相关的其他依赖
|
||||
- `pnpm up:uniapp` 升级 `uni-app` 相关的依赖
|
||||
|
||||
推荐先使用 `pnpm up:pkg` 升级, 再使用 `pnpm up:uniapp` 进行升级,因为 `pnpm up:uniapp` 很有可能会进行版本的降级已达到和 `uni-app` 版本匹配的效果
|
||||
|
||||
## 切换镜像源
|
||||
|
||||
默认情况下,走的是淘宝镜像源 : `registry.npmmirror.com`
|
||||
|
||||
假如你需要修改镜像源,请修改目录下的 `.npmrc` 文件,然后重新进行 `pnpm i` 安装包即可
|
||||
|
||||
## 包管理器
|
||||
|
||||
本项目默认使用 `pnpm@10` 进行管理,当然你也可以切换到其他包管理器,比如 `yarn`, `npm`
|
||||
|
||||
你只需要把 `pnpm-lock.yaml` 删掉,然后把 `package.json` 中的 `packageManager` 字段去除或者换成你具体的包管理器版本,然后重新安装即可
|
||||
|
||||
### weapp-ide-cli
|
||||
|
||||
本项目已经集成 `weapp-ide-cli` 可以通过 `cli` 对 `ide` 进行额外操作
|
||||
|
||||
- `pnpm open:dev` 打开微信开发者工具,引入 `dist/dev/mp-weixin`
|
||||
- `pnpm open:build` 打开微信开发者工具,引入 `dist/build/mp-weixin`
|
||||
|
||||
[详细信息](https://www.npmjs.com/package/weapp-ide-cli)
|
||||
|
||||
## tailwindcss 生态
|
||||
|
||||
详见:<https://github.com/aniftyco/awesome-tailwindcss>
|
||||
|
||||
你可以在这里找到许多现成的UI,组件模板。
|
||||
|
||||
## 单位转换
|
||||
|
||||
- `rem` -> `rpx` (默认开启, 见 `vite.config.ts` 中 `uvtw` 插件的 `rem2rpx` 选项)
|
||||
- `px` -> `rpx` (默认不开启,可在 `postcss.config.ts` 中引入 `postcss-pxtransform` 开启配置)
|
||||
|
||||
## Tips
|
||||
|
||||
- 升级 `uni-app` 依赖的方式为 `npx @dcloudio/uvm` 后,选择对应的 `Package Manager` 即可。而升级其他包的方式,可以使用 `pnpm up -Li`,这个是 `pnpm` 自带的方式。
|
||||
- 使用 `vscode` 记得安装官方插件 `stylelint`,`tailwindcss`, 已在 `.vscode/extensions.json` 中设置推荐
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport =
|
||||
'CSS' in window &&
|
||||
typeof CSS.supports === 'function' &&
|
||||
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') +
|
||||
'" />',
|
||||
)
|
||||
</script>
|
||||
<style>
|
||||
html {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
</style>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
# netlify 部署文件
|
||||
# 用于部署此项目的 h5 产物,可删除
|
||||
[build]
|
||||
publish = "dist/build/h5"
|
||||
command = "pnpm run build:h5"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "ruoyi-fastapi-app",
|
||||
"version": "1.9.0",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"scripts": {
|
||||
"dev": "uni -p mp-weixin",
|
||||
"build": "npm run build:mp-weixin",
|
||||
"dev:app": "uni -p app",
|
||||
"dev:custom": "uni -p",
|
||||
"dev:h5": "uni --host",
|
||||
"dev:h5:ssr": "uni --ssr",
|
||||
"dev:mp-alipay": "uni -p mp-alipay",
|
||||
"dev:mp-baidu": "uni -p mp-baidu",
|
||||
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||
"dev:mp-lark": "uni -p mp-lark",
|
||||
"dev:mp-qq": "uni -p mp-qq",
|
||||
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||
"build:app": "uni build -p app",
|
||||
"build:custom": "uni build -p",
|
||||
"build:h5": "uni build",
|
||||
"build:h5:ssr": "uni build --ssr",
|
||||
"build:mp-alipay": "uni build -p mp-alipay",
|
||||
"build:mp-baidu": "uni build -p mp-baidu",
|
||||
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||
"build:mp-lark": "uni build -p mp-lark",
|
||||
"build:mp-qq": "uni build -p mp-qq",
|
||||
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"open:dev": "weapp open -p dist/dev/mp-weixin",
|
||||
"open:build": "weapp open -p dist/build/mp-weixin",
|
||||
"weapp:login": "weapp login",
|
||||
"upload:dev": "weapp upload -p dist/dev/mp-weixin -v 1.0.0 -d \"dev version\"",
|
||||
"upload:build": "weapp upload -p dist/build/mp-weixin -v 1.0.0 -d \"release version\"",
|
||||
"postinstall": "weapp-tw patch",
|
||||
"up:pkg": "pnpm up -rLi \"!@dcloudio/*\"",
|
||||
"up:uniapp": "pnpx @dcloudio/uvm@latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-components": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-harmony": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4080720251210001",
|
||||
"@vue/shared": "3.4.21",
|
||||
"@weapp-tailwindcss/merge-v3": "^0.1.5",
|
||||
"pinia": "2.2.4",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
|
||||
"@egoist/tailwindcss-icons": "^1.9.2",
|
||||
"@icebreakers/stylelint-config": "^1.2.5",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/svg-spinners": "^1.2.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vue/runtime-core": "^3.5.27",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "^1.97.3",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"vite": "5.4.21",
|
||||
"weapp-ide-cli": "^5.0.1",
|
||||
"weapp-tailwindcss": "^4.9.8"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"weapp-tailwindcss",
|
||||
"@weapp-tailwindcss/merge-v3"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import process from "node:process";
|
||||
|
||||
const isH5 = process.env.UNI_PLATFORM === "h5";
|
||||
const isApp = process.env.UNI_PLATFORM === "app";
|
||||
const WeappTailwindcssDisabled = isH5 || isApp;
|
||||
const isMp = !isH5 && !isApp;
|
||||
|
||||
export { isApp, isH5, isMp, WeappTailwindcssDisabled };
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { AcceptedPlugin } from "postcss";
|
||||
import autoprefixer from "autoprefixer";
|
||||
import tailwindcss from "tailwindcss";
|
||||
import cssMacro from "weapp-tailwindcss/css-macro/postcss";
|
||||
|
||||
const plugins: AcceptedPlugin[] = [tailwindcss(), autoprefixer()];
|
||||
|
||||
// 可以使用 postcss-pxtransform 来进行 px 转 rpx 的功能
|
||||
// 详见: https://tw.icebreaker.top/docs/quick-start/css-unit-transform#px-%E8%BD%AC-rpx
|
||||
|
||||
plugins.push(cssMacro);
|
||||
|
||||
export default plugins;
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import config from "./config";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { useConfigStore } from "@/store";
|
||||
import { getCurrentInstance } from "vue";
|
||||
import { onLaunch } from "@dcloudio/uni-app";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
onLaunch(() => {
|
||||
initApp();
|
||||
});
|
||||
|
||||
// 初始化应用
|
||||
function initApp() {
|
||||
// 初始化应用配置
|
||||
initConfig();
|
||||
// 检查用户登录状态
|
||||
//#ifdef H5
|
||||
checkLogin();
|
||||
//#endif
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
useConfigStore().setConfig(config);
|
||||
}
|
||||
|
||||
function checkLogin() {
|
||||
if (!getToken()) {
|
||||
proxy.$tab.reLaunch("/pages/login");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Global Reset for UniApp/Mobile */
|
||||
page,
|
||||
body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
/* Prevent default bounce effect on iOS if needed, or just handle overflow */
|
||||
}
|
||||
|
||||
/* Global box-sizing for consistency with Tailwind preflight */
|
||||
page,
|
||||
view,
|
||||
scroll-view,
|
||||
image,
|
||||
text,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
label,
|
||||
navigator {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 修复 H5 端 uni.showToast 图标在引入 Tailwind 后可能偏左的问题 */
|
||||
/* #ifdef H5 */
|
||||
uni-toast img,
|
||||
uni-toast svg {
|
||||
display: inline-block !important;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
/* Hide scrollbar for Chrome/Safari/Webkit */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 登录方法
|
||||
export function login(username, password, code, uuid) {
|
||||
const data = {
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
uuid,
|
||||
};
|
||||
return request({
|
||||
url: "/login",
|
||||
header: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
headers: {
|
||||
isToken: false,
|
||||
},
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 注册方法
|
||||
export function register(data) {
|
||||
return request({
|
||||
url: "/register",
|
||||
headers: {
|
||||
isToken: false,
|
||||
},
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
export function getInfo() {
|
||||
return request({
|
||||
url: "/getInfo",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 退出方法
|
||||
export function logout() {
|
||||
return request({
|
||||
url: "/logout",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
export function getCodeImg() {
|
||||
return request({
|
||||
url: "/captchaImage",
|
||||
headers: {
|
||||
isToken: false,
|
||||
},
|
||||
method: "get",
|
||||
timeout: 20000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 查询字典数据列表
|
||||
export function listData(query) {
|
||||
return request({
|
||||
url: "/system/dict/data/list",
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询字典数据详细
|
||||
export function getData(dictCode) {
|
||||
return request({
|
||||
url: "/system/dict/data/" + dictCode,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 根据字典类型查询字典数据信息
|
||||
export function getDicts(dictType) {
|
||||
return request({
|
||||
url: "/system/dict/data/type/" + dictType,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 新增字典数据
|
||||
export function addData(data) {
|
||||
return request({
|
||||
url: "/system/dict/data",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改字典数据
|
||||
export function updateData(data) {
|
||||
return request({
|
||||
url: "/system/dict/data",
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除字典数据
|
||||
export function delData(dictCode) {
|
||||
return request({
|
||||
url: "/system/dict/data/" + dictCode,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 查询字典类型列表
|
||||
export function listType(query) {
|
||||
return request({
|
||||
url: "/system/dict/type/list",
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询字典类型详细
|
||||
export function getType(dictId) {
|
||||
return request({
|
||||
url: "/system/dict/type/" + dictId,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 新增字典类型
|
||||
export function addType(data) {
|
||||
return request({
|
||||
url: "/system/dict/type",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改字典类型
|
||||
export function updateType(data) {
|
||||
return request({
|
||||
url: "/system/dict/type",
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
export function delType(dictId) {
|
||||
return request({
|
||||
url: "/system/dict/type/" + dictId,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新字典缓存
|
||||
export function refreshCache() {
|
||||
return request({
|
||||
url: "/system/dict/type/refreshCache",
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取字典选择框列表
|
||||
export function optionselect() {
|
||||
return request({
|
||||
url: "/system/dict/type/optionselect",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import upload from "@/utils/upload";
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 用户密码重置
|
||||
export function updateUserPwd(oldPassword, newPassword) {
|
||||
const data = {
|
||||
oldPassword,
|
||||
newPassword,
|
||||
};
|
||||
return request({
|
||||
url: "/system/user/profile/updatePwd",
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询用户个人信息
|
||||
export function getUserProfile() {
|
||||
return request({
|
||||
url: "/system/user/profile",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 修改用户个人信息
|
||||
export function updateUserProfile(data) {
|
||||
return request({
|
||||
url: "/system/user/profile",
|
||||
method: "put",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 用户头像上传
|
||||
export function uploadAvatar(data) {
|
||||
return upload({
|
||||
url: "/system/user/profile/avatar",
|
||||
name: data.name,
|
||||
filePath: data.filePath,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const createPinia: typeof import('pinia').createPinia
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const defineStore: typeof import('pinia').defineStore
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const getActivePinia: typeof import('pinia').getActivePinia
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const h: typeof import('vue').h
|
||||
const inject: typeof import('vue').inject
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const mapActions: typeof import('pinia').mapActions
|
||||
const mapGetters: typeof import('pinia').mapGetters
|
||||
const mapState: typeof import('pinia').mapState
|
||||
const mapStores: typeof import('pinia').mapStores
|
||||
const mapWritableState: typeof import('pinia').mapWritableState
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onAddToFavorites: typeof import('@dcloudio/uni-app').onAddToFavorites
|
||||
const onBackPress: typeof import('@dcloudio/uni-app').onBackPress
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onError: typeof import('@dcloudio/uni-app').onError
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onHide: typeof import('@dcloudio/uni-app').onHide
|
||||
const onLaunch: typeof import('@dcloudio/uni-app').onLaunch
|
||||
const onLoad: typeof import('@dcloudio/uni-app').onLoad
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app').onNavigationBarButtonTap
|
||||
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputChanged
|
||||
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputClicked
|
||||
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputConfirmed
|
||||
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputFocusChanged
|
||||
const onPageNotFound: typeof import('@dcloudio/uni-app').onPageNotFound
|
||||
const onPageScroll: typeof import('@dcloudio/uni-app').onPageScroll
|
||||
const onPullDownRefresh: typeof import('@dcloudio/uni-app').onPullDownRefresh
|
||||
const onReachBottom: typeof import('@dcloudio/uni-app').onReachBottom
|
||||
const onReady: typeof import('@dcloudio/uni-app').onReady
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onResize: typeof import('@dcloudio/uni-app').onResize
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onShareAppMessage: typeof import('@dcloudio/uni-app').onShareAppMessage
|
||||
const onShareTimeline: typeof import('@dcloudio/uni-app').onShareTimeline
|
||||
const onShow: typeof import('@dcloudio/uni-app').onShow
|
||||
const onTabItemTap: typeof import('@dcloudio/uni-app').onTabItemTap
|
||||
const onThemeChange: typeof import('@dcloudio/uni-app').onThemeChange
|
||||
const onUnhandledRejection: typeof import('@dcloudio/uni-app').onUnhandledRejection
|
||||
const onUnload: typeof import('@dcloudio/uni-app').onUnload
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const provide: typeof import('vue').provide
|
||||
const reactive: typeof import('vue').reactive
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const setActivePinia: typeof import('pinia').setActivePinia
|
||||
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const storeToRefs: typeof import('pinia').storeToRefs
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const unref: typeof import('vue').unref
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useId: typeof import('vue').useId
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const watch: typeof import('vue').watch
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 应用全局配置
|
||||
export default {
|
||||
baseUrl: "http://localhost:9099",
|
||||
// 应用信息
|
||||
appInfo: {
|
||||
// 应用名称
|
||||
name: "RuoYi-FastAPI-APP",
|
||||
// 应用版本
|
||||
version: "1.9.0",
|
||||
// 应用logo
|
||||
logo: "/static/logo.png",
|
||||
// 官方网站
|
||||
site_url: "https://vfadmin.insistence.tech",
|
||||
// 政策协议
|
||||
agreements: [
|
||||
{
|
||||
title: "隐私政策",
|
||||
url: "https://ruoyi.vip/protocol.html",
|
||||
},
|
||||
{
|
||||
title: "用户服务协议",
|
||||
url: "https://ruoyi.vip/protocol.html",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
|
||||
const component: DefineComponent<object, object, any>;
|
||||
export default component;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// @ts-nocheck
|
||||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import store from "./store"; // store
|
||||
import { install } from "./plugins"; // plugins
|
||||
import "./permission"; // permission
|
||||
import { useDict } from "@/utils/dict";
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
app.use(store);
|
||||
app.config.globalProperties.useDict = useDict;
|
||||
install(app);
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "RuoYi-FastAPI移动端",
|
||||
"appid": "__UNI__25A9D80",
|
||||
"description": "",
|
||||
"versionName": "1.9.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueCompiler": "uni-app",
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"modules": {},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
"ios": {
|
||||
"dSYMs": false
|
||||
},
|
||||
"sdkConfigs": {}
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "wxccd7e2a0911b3397",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": false,
|
||||
"minified": true,
|
||||
"postcss": true
|
||||
},
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
},
|
||||
"usingComponents": true
|
||||
},
|
||||
"vueVersion": "3",
|
||||
"h5": {
|
||||
"devServer": {
|
||||
"port": 9090,
|
||||
"https": false
|
||||
},
|
||||
"title": "RuoYi-FastAPI-APP",
|
||||
"router": {
|
||||
"mode": "hash",
|
||||
"base": "./"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "注册"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "RuoYi-FastAPI移动端",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/work/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/avatar/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "修改头像"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/info/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人信息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/info/edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑资料"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/pwd/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "修改密码"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/setting/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "应用设置"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/help/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "常见问题"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/about/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于我们"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/common/agreement/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户协议"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/common/privacy/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "隐私协议"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/common/webview/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "浏览网页"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/common/textview/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "浏览文本"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#000000",
|
||||
"selectedColor": "#000000",
|
||||
"borderStyle": "white",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index",
|
||||
"iconPath": "static/images/tabbar/home.png",
|
||||
"selectedIconPath": "static/images/tabbar/home_.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/work/index",
|
||||
"iconPath": "static/images/tabbar/work.png",
|
||||
"selectedIconPath": "static/images/tabbar/work_.png",
|
||||
"text": "工作台"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index",
|
||||
"iconPath": "static/images/tabbar/mine.png",
|
||||
"selectedIconPath": "static/images/tabbar/mine_.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "RuoYi",
|
||||
"navigationBarBackgroundColor": "#FFFFFF"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view class="min-h-full bg-white px-5 py-8">
|
||||
<view class="mb-6 text-center">
|
||||
<text class="text-xl font-bold text-gray-900">用户协议</text>
|
||||
</view>
|
||||
|
||||
<view class="space-y-6 text-justify text-sm leading-relaxed text-gray-600">
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800"
|
||||
>1. 服务条款的确认和接纳</text
|
||||
>
|
||||
<text
|
||||
>欢迎使用RuoYi-FastAPI移动端框架(以下简称“本产品”)。本产品的服务所有权和运作权归RuoYi-FastAPI所有。用户在使用本产品前,请仔细阅读本协议。当您点击“同意”或实际使用本产品服务时,即视为您已阅读并完全同意本协议的所有条款。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800"
|
||||
>2. 用户账号与安全</text
|
||||
>
|
||||
<text
|
||||
>您在注册账号时,应提供真实、准确、最新和完整的个人资料。您有责任维护账号和密码的安全,并对以您账号进行的所有活动承担法律责任。如发现账号被非法使用,请立即通知我们。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800">3. 用户行为规范</text>
|
||||
<text
|
||||
>用户在使用本产品过程中,必须遵守国家法律法规,不得利用本产品从事违法违规活动,包括但不限于:发布虚假信息、侵犯他人知识产权、传播淫秽色情信息、进行网络攻击等。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800"
|
||||
>4. 服务内容的变更、中断或终止</text
|
||||
>
|
||||
<text
|
||||
>鉴于网络服务的特殊性,我们有权随时变更、中断或终止部分或全部网络服务。对于因服务器维护、网络故障等原因导致的服务中断,我们将尽量提前公告,但不对因此产生的损失承担责任。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800">5. 知识产权声明</text>
|
||||
<text
|
||||
>本产品包含的所有内容(包括但不限于文字、图片、音频、视频、软件代码等)的知识产权归RuoYi-FastAPI或相关权利人所有。未经授权,任何人不得擅自使用、复制、修改或传播。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800">6. 免责声明</text>
|
||||
<text
|
||||
>本产品按“现状”提供,不包含任何明示或暗示的保证。我们不对因不可抗力、黑客攻击、系统不稳定等原因导致的服务中断或数据丢失承担责任。</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-10 text-center text-xs text-gray-400">
|
||||
<text>更新日期:2024年01月01日</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<view class="min-h-full bg-white px-5 py-8">
|
||||
<view class="mb-6 text-center">
|
||||
<text class="text-xl font-bold text-gray-900">隐私协议</text>
|
||||
</view>
|
||||
|
||||
<view class="space-y-6 text-justify text-sm leading-relaxed text-gray-600">
|
||||
<view>
|
||||
<text
|
||||
>RuoYi-FastAPI非常重视您的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的个人信息。请您在使用本产品前仔细阅读本政策。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800"
|
||||
>1. 我们如何收集和使用您的个人信息</text
|
||||
>
|
||||
<text>为了向您提供更好的服务,我们可能会收集您的以下信息:</text>
|
||||
<text class="mt-1 block pl-2"
|
||||
>•
|
||||
账号信息:当您注册或登录时,我们需要收集您的用户名、密码、手机号码等信息。</text
|
||||
>
|
||||
<text class="mt-1 block pl-2"
|
||||
>•
|
||||
设备信息:为了保障账户安全和优化服务,我们会收集您的设备型号、操作系统版本、唯一设备标识符等信息。</text
|
||||
>
|
||||
<text class="mt-1 block pl-2"
|
||||
>•
|
||||
日志信息:当您使用本产品时,我们会自动收集您的操作日志、IP地址、访问时间等信息。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800"
|
||||
>2. 我们如何共享、转让、公开披露您的个人信息</text
|
||||
>
|
||||
<text
|
||||
>我们不会向任何第三方共享、转让或公开披露您的个人信息,但以下情况除外:</text
|
||||
>
|
||||
<text class="mt-1 block pl-2">• 获得您的明确授权或同意;</text>
|
||||
<text class="mt-1 block pl-2">• 根据法律法规或司法机关的要求;</text>
|
||||
<text class="mt-1 block pl-2"
|
||||
>• 为了维护国家安全、公共安全或重大公共利益。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800"
|
||||
>3. 我们如何保护您的个人信息</text
|
||||
>
|
||||
<text
|
||||
>我们采取了多种安全措施来保护您的个人信息,包括数据加密、访问控制、安全审计等。我们会尽力防止您的信息被未经授权的访问、篡改或泄露。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800">4. 您的权利</text>
|
||||
<text
|
||||
>您有权访问、更正或删除您的个人信息。您也可以随时注销账号。如您需要行使这些权利,请通过“联系我们”章节中的方式与我们联系。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800"
|
||||
>5. 本政策如何更新</text
|
||||
>
|
||||
<text
|
||||
>随着法律法规的变化或业务的发展,我们可能会不时更新本隐私政策。更新后的政策将在本产品中发布,并在生效前通过适当方式通知您。</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<text class="mb-2 block font-bold text-gray-800">6. 联系我们</text>
|
||||
<text
|
||||
>如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:</text
|
||||
>
|
||||
<text class="mt-1 block">邮箱:xxx@xxx.com</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-10 text-center text-xs text-gray-400">
|
||||
<text>更新日期:2024年01月01日</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view class="h-full overflow-y-auto p-4">
|
||||
<view class="bg-white rounded-xl shadow-sm p-4">
|
||||
<view
|
||||
class="text-lg font-bold text-gray-900 mb-3 border-b border-gray-100 pb-2"
|
||||
>
|
||||
{{ title }}
|
||||
</view>
|
||||
<text class="block text-sm text-gray-600 leading-relaxed">
|
||||
{{ content }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
const title = ref("");
|
||||
const content = ref("");
|
||||
|
||||
onLoad((options) => {
|
||||
title.value = options.title;
|
||||
content.value = options.content;
|
||||
uni.setNavigationBarTitle({
|
||||
title: options.title,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<view v-if="params.url">
|
||||
<web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
const params = ref({});
|
||||
const webviewStyles = ref({
|
||||
progress: {
|
||||
color: "#FF3333",
|
||||
},
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
onLoad((event) => {
|
||||
params.value = event;
|
||||
if (event.title) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: event.title,
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col bg-gray-50">
|
||||
<!-- Header Area -->
|
||||
<view class="bg-white pb-6 rounded-b-3xl shadow-sm z-50">
|
||||
<!-- Title -->
|
||||
<view class="pt-12 pb-4 px-4 flex justify-center items-center">
|
||||
<text class="text-lg font-bold text-gray-800">RuoYi-FastAPI移动端</text>
|
||||
</view>
|
||||
|
||||
<!-- Search & Notification -->
|
||||
<view class="px-4 pb-2">
|
||||
<view class="flex items-center space-x-3">
|
||||
<!-- Search Input -->
|
||||
<view class="flex-1 relative">
|
||||
<view
|
||||
class="absolute left-3 top-0 bottom-0 flex items-center text-gray-400"
|
||||
>
|
||||
<view class="i-mdi-magnify text-lg"></view>
|
||||
</view>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索..."
|
||||
placeholder-class="text-gray-400"
|
||||
class="w-full h-10 rounded-full bg-gray-100 pl-10 pr-4 text-sm text-gray-800"
|
||||
/>
|
||||
</view>
|
||||
<!-- Notification -->
|
||||
<view
|
||||
class="relative flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 text-gray-600 active:scale-95 transition-transform border border-gray-100"
|
||||
>
|
||||
<view class="i-mdi-bell-outline text-xl"></view>
|
||||
<view
|
||||
class="absolute top-2.5 right-2.5 h-2 w-2 rounded-full bg-red-500 border border-white"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="flex-1 overflow-hidden"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="p-4 space-y-5 pb-24">
|
||||
<!-- Hero Card -->
|
||||
<view
|
||||
class="relative overflow-hidden rounded-3xl bg-gradient-to-r from-indigo-500 to-purple-600 p-6 text-white shadow-xl shadow-indigo-200"
|
||||
>
|
||||
<view
|
||||
class="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-white/20 blur-2xl"
|
||||
></view>
|
||||
<view
|
||||
class="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-white/20 blur-2xl"
|
||||
></view>
|
||||
|
||||
<view class="relative z-10">
|
||||
<view class="mb-4 flex items-center justify-between">
|
||||
<view
|
||||
class="rounded-lg bg-white/20 px-3 py-1 text-xs font-medium backdrop-blur-sm"
|
||||
>系统运行正常</view
|
||||
>
|
||||
<view class="text-xs opacity-80">刚刚更新</view>
|
||||
</view>
|
||||
<view class="flex items-end justify-between">
|
||||
<view>
|
||||
<view class="text-3xl font-bold">85%</view>
|
||||
<view class="text-sm opacity-90">工作效率</view>
|
||||
</view>
|
||||
<view
|
||||
class="h-10 w-24 overflow-hidden rounded-lg bg-white/10 p-1"
|
||||
>
|
||||
<view class="h-full w-full flex items-end justify-between px-1">
|
||||
<view class="w-1 bg-white/50 h-2 rounded-full"></view>
|
||||
<view class="w-1 bg-white/70 h-4 rounded-full"></view>
|
||||
<view class="w-1 bg-white/40 h-3 rounded-full"></view>
|
||||
<view class="w-1 bg-white/90 h-6 rounded-full"></view>
|
||||
<view class="w-1 bg-white/60 h-4 rounded-full"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view
|
||||
class="rounded-2xl bg-white p-4 shadow-sm border border-gray-100 flex items-center justify-between"
|
||||
>
|
||||
<view>
|
||||
<view class="text-xs text-gray-500 mb-1">新通知</view>
|
||||
<view class="text-2xl font-bold text-gray-800">12</view>
|
||||
</view>
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-bell text-xl"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="rounded-2xl bg-white p-4 shadow-sm border border-gray-100 flex items-center justify-between"
|
||||
>
|
||||
<view>
|
||||
<view class="text-xs text-gray-500 mb-1">待办任务</view>
|
||||
<view class="text-2xl font-bold text-gray-800">5</view>
|
||||
</view>
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-full bg-purple-50 text-purple-500"
|
||||
>
|
||||
<view class="i-mdi-calendar text-xl"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Quick Functions -->
|
||||
<view>
|
||||
<view class="mb-4 flex items-center justify-between">
|
||||
<view class="text-base font-bold text-gray-800">快捷入口</view>
|
||||
</view>
|
||||
<view
|
||||
class="grid grid-cols-4 gap-2 rounded-2xl bg-white p-4 shadow-sm"
|
||||
>
|
||||
<view class="flex flex-col items-center space-y-2">
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-orange-50 text-orange-500"
|
||||
>
|
||||
<view class="i-mdi-file-document-edit text-xl"></view>
|
||||
</view>
|
||||
<view class="text-xs font-medium text-gray-600">申请</view>
|
||||
</view>
|
||||
<view class="flex flex-col items-center space-y-2">
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-green-50 text-green-500"
|
||||
>
|
||||
<view class="i-mdi-newspaper text-xl"></view>
|
||||
</view>
|
||||
<view class="text-xs font-medium text-gray-600">新闻</view>
|
||||
</view>
|
||||
<view class="flex flex-col items-center space-y-2">
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-indigo-50 text-indigo-500"
|
||||
>
|
||||
<view class="i-mdi-chart-bar text-xl"></view>
|
||||
</view>
|
||||
<view class="text-xs font-medium text-gray-600">统计</view>
|
||||
</view>
|
||||
<view class="flex flex-col items-center space-y-2">
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-50 text-gray-400"
|
||||
>
|
||||
<view class="i-mdi-calendar text-xl"></view>
|
||||
</view>
|
||||
<view class="text-xs font-medium text-gray-600">计划</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Recent Apps -->
|
||||
<view>
|
||||
<view class="mb-4 flex items-center justify-between">
|
||||
<view class="text-base font-bold text-gray-800">最近更新</view>
|
||||
<view class="text-xs text-blue-500">查看全部</view>
|
||||
</view>
|
||||
<view class="space-y-3">
|
||||
<view
|
||||
class="flex items-center rounded-2xl bg-white p-4 shadow-sm border border-gray-50"
|
||||
>
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-50 text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-clipboard-text-outline text-xl"></view>
|
||||
</view>
|
||||
<view class="ml-4 flex-1">
|
||||
<view class="font-bold text-gray-800">请假申请</view>
|
||||
<view class="text-xs text-gray-500">等待经理审批</view>
|
||||
</view>
|
||||
<view
|
||||
class="rounded-full bg-yellow-50 px-3 py-1 text-xs font-medium text-yellow-600"
|
||||
>待处理</view
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="flex items-center rounded-2xl bg-white p-4 shadow-sm border border-gray-50"
|
||||
>
|
||||
<view
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-green-50 text-green-500"
|
||||
>
|
||||
<view class="i-mdi-newspaper text-xl"></view>
|
||||
</view>
|
||||
<view class="ml-4 flex-1">
|
||||
<view class="font-bold text-gray-800">周报</view>
|
||||
<view class="text-xs text-gray-500">提交成功</view>
|
||||
</view>
|
||||
<view
|
||||
class="rounded-full bg-green-50 px-3 py-1 text-xs font-medium text-green-600"
|
||||
>已完成</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const globalConfig = getApp().globalData.config;
|
||||
</script>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<view
|
||||
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
|
||||
>
|
||||
<!-- Logo Section -->
|
||||
<view class="mb-6 flex flex-col items-center">
|
||||
<view
|
||||
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
|
||||
>
|
||||
<image
|
||||
class="size-10"
|
||||
:src="globalConfig.appInfo.logo"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-xl font-bold tracking-wide text-gray-800"
|
||||
>RuoYi-FastAPI移动端登录</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- Form Section -->
|
||||
<view class="w-full rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur-md">
|
||||
<!-- Username -->
|
||||
<view class="group relative mb-5">
|
||||
<view
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-account text-xl"></view>
|
||||
</view>
|
||||
<input
|
||||
v-model="loginForm.username"
|
||||
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
|
||||
type="text"
|
||||
placeholder="请输入账号"
|
||||
maxlength="30"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Password -->
|
||||
<view class="group relative mb-5">
|
||||
<view
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-lock text-xl"></view>
|
||||
</view>
|
||||
<input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
|
||||
placeholder="请输入密码"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Captcha -->
|
||||
<view
|
||||
class="mb-8 flex items-center justify-between"
|
||||
v-if="captchaEnabled"
|
||||
>
|
||||
<view class="group relative mr-3 flex-1">
|
||||
<view
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-security text-xl"></view>
|
||||
</view>
|
||||
<input
|
||||
v-model="loginForm.code"
|
||||
type="number"
|
||||
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
|
||||
placeholder="验证码"
|
||||
maxlength="4"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="h-12 w-28 overflow-hidden rounded-xl bg-gray-100 shadow-sm transition-opacity active:opacity-80"
|
||||
@click="getCode"
|
||||
>
|
||||
<image :src="codeUrl" class="size-full object-cover"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Login Button -->
|
||||
<button
|
||||
@click="handleLogin"
|
||||
class="flex h-12 w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-transform active:scale-95"
|
||||
>
|
||||
登 录
|
||||
</button>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<view class="mt-6 flex flex-col items-center space-y-3">
|
||||
<view class="flex items-center text-sm text-gray-500" v-if="register">
|
||||
<text>没有账号?</text>
|
||||
<text
|
||||
@click="handleUserRegister"
|
||||
class="ml-1 font-medium text-blue-600 active:opacity-70"
|
||||
>立即注册</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="flex flex-wrap items-center justify-center text-xs text-gray-400"
|
||||
>
|
||||
<text>登录即代表同意</text>
|
||||
<text
|
||||
@click="handleUserAgrement"
|
||||
class="mx-1 text-blue-500 active:opacity-70"
|
||||
>《用户协议》</text
|
||||
>
|
||||
<text>和</text>
|
||||
<text
|
||||
@click="handlePrivacy"
|
||||
class="mx-1 text-blue-500 active:opacity-70"
|
||||
>《隐私协议》</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { getCodeImg } from "@/api/login";
|
||||
import { useConfigStore, useUserStore } from "@/store";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const globalConfig = useConfigStore().config;
|
||||
const codeUrl = ref("");
|
||||
// 验证码开关
|
||||
const captchaEnabled = ref(true);
|
||||
// 用户注册开关
|
||||
const register = ref(false);
|
||||
const loginForm = ref({
|
||||
username: "admin",
|
||||
password: "admin123",
|
||||
code: "",
|
||||
uuid: "",
|
||||
});
|
||||
|
||||
// 用户注册
|
||||
function handleUserRegister() {
|
||||
proxy.$tab.redirectTo(`/pages/register`);
|
||||
}
|
||||
|
||||
// 隐私协议
|
||||
function handlePrivacy() {
|
||||
proxy.$tab.navigateTo(`/pages/common/privacy/index`);
|
||||
}
|
||||
|
||||
// 用户协议
|
||||
function handleUserAgrement() {
|
||||
proxy.$tab.navigateTo(`/pages/common/agreement/index`);
|
||||
}
|
||||
|
||||
// 获取图形验证码
|
||||
function getCode() {
|
||||
getCodeImg().then((res) => {
|
||||
captchaEnabled.value =
|
||||
res.captchaEnabled === undefined ? true : res.captchaEnabled;
|
||||
if (captchaEnabled.value) {
|
||||
codeUrl.value = "data:image/gif;base64," + res.img;
|
||||
loginForm.value.uuid = res.uuid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 登录方法
|
||||
async function handleLogin() {
|
||||
if (loginForm.value.username === "") {
|
||||
proxy.$modal.msgError("请输入账号");
|
||||
} else if (loginForm.value.password === "") {
|
||||
proxy.$modal.msgError("请输入密码");
|
||||
} else if (loginForm.value.code === "" && captchaEnabled.value) {
|
||||
proxy.$modal.msgError("请输入验证码");
|
||||
} else {
|
||||
proxy.$modal.loading("登录中,请耐心等待...");
|
||||
pwdLogin();
|
||||
}
|
||||
}
|
||||
|
||||
// 密码登录
|
||||
async function pwdLogin() {
|
||||
useUserStore()
|
||||
.login(loginForm.value)
|
||||
.then(() => {
|
||||
proxy.$modal.closeLoading();
|
||||
loginSuccess();
|
||||
})
|
||||
.catch(() => {
|
||||
if (captchaEnabled.value) {
|
||||
getCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 登录成功后,处理函数
|
||||
function loginSuccess(result) {
|
||||
// 设置用户信息
|
||||
useUserStore()
|
||||
.getInfo()
|
||||
.then((res) => {
|
||||
proxy.$tab.reLaunch("/pages/index");
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
//#ifdef H5
|
||||
if (getToken()) {
|
||||
proxy.$tab.reLaunch("/pages/index");
|
||||
}
|
||||
//#endif
|
||||
});
|
||||
|
||||
getCode();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col bg-gray-50 pb-10">
|
||||
<!-- Header -->
|
||||
<view class="flex flex-col items-center py-10">
|
||||
<image
|
||||
class="size-20 rounded-xl shadow-md"
|
||||
src="/static/logo200.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<text class="mt-4 text-xl font-bold text-gray-800"
|
||||
>RuoYi-FastAPI移动端</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- Info List -->
|
||||
<view class="bg-white">
|
||||
<!-- Version -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
|
||||
>
|
||||
<text class="text-base text-gray-800">版本信息</text>
|
||||
<text class="text-sm text-gray-500">v{{ version }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Email -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
|
||||
>
|
||||
<text class="text-base text-gray-800">官方邮箱</text>
|
||||
<text class="text-sm text-gray-500">xxx@xxx.com</text>
|
||||
</view>
|
||||
|
||||
<!-- Hotline -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
|
||||
>
|
||||
<text class="text-base text-gray-800">服务热线</text>
|
||||
<text class="text-sm text-gray-500">400-999-9999</text>
|
||||
</view>
|
||||
|
||||
<!-- Website -->
|
||||
<view class="flex items-center justify-between px-5 py-4">
|
||||
<text class="text-base text-gray-800">公司网站</text>
|
||||
<text class="text-sm text-blue-500 underline active:opacity-70">{{
|
||||
url
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Copyright -->
|
||||
<view class="mt-10 text-center text-xs text-gray-400">
|
||||
Copyright © 2026 insistence.tech All Rights Reserved.
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useConfigStore } from "@/store";
|
||||
|
||||
const url = useConfigStore().config.appInfo.site_url;
|
||||
const version = useConfigStore().config.appInfo.version;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,678 @@
|
||||
<template>
|
||||
<view class="h-full bg-black">
|
||||
<view class="relative h-full w-full">
|
||||
<view class="min-h-[750rpx] w-full">
|
||||
<view
|
||||
v-if="isShowImg"
|
||||
class="relative box-border overflow-hidden"
|
||||
:style="
|
||||
'width:' +
|
||||
cropperInitW +
|
||||
'px;height:' +
|
||||
cropperInitH +
|
||||
'px;background:#000'
|
||||
"
|
||||
>
|
||||
<view
|
||||
class="relative"
|
||||
:style="
|
||||
'width:' +
|
||||
cropperW +
|
||||
'px;height:' +
|
||||
cropperH +
|
||||
'px;left:' +
|
||||
cropperL +
|
||||
'px;top:' +
|
||||
cropperT +
|
||||
'px'
|
||||
"
|
||||
>
|
||||
<image
|
||||
:src="imageSrc"
|
||||
:style="
|
||||
'width:' +
|
||||
cropperW +
|
||||
'px;height:' +
|
||||
cropperH +
|
||||
'px;display:block;margin:0 auto;'
|
||||
"
|
||||
></image>
|
||||
<view
|
||||
class="absolute z-10 bg-white/30"
|
||||
@touchstart.stop="contentStartMove"
|
||||
@touchmove.stop="contentMoveing"
|
||||
@touchend.stop="contentTouchEnd"
|
||||
:style="
|
||||
'left:' +
|
||||
cutL +
|
||||
'px;top:' +
|
||||
cutT +
|
||||
'px;right:' +
|
||||
cutR +
|
||||
'px;bottom:' +
|
||||
cutB +
|
||||
'px'
|
||||
"
|
||||
>
|
||||
<view
|
||||
class="relative block h-full w-full overflow-visible outline outline-1 outline-[rgba(102,153,255,0.75)]"
|
||||
>
|
||||
<view class="uni-cropper-dashed-h"></view>
|
||||
<view class="uni-cropper-dashed-v"></view>
|
||||
<view
|
||||
class="uni-cropper-line-t"
|
||||
data-drag="top"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-line-r"
|
||||
data-drag="right"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-line-b"
|
||||
data-drag="bottom"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-line-l"
|
||||
data-drag="left"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-t"
|
||||
data-drag="top"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-tr"
|
||||
data-drag="topTight"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-r"
|
||||
data-drag="right"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-rb"
|
||||
data-drag="rightBottom"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-b"
|
||||
data-drag="bottom"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-bl"
|
||||
data-drag="bottomLeft"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-l"
|
||||
data-drag="left"
|
||||
@touchstart.stop="dragStart"
|
||||
@touchmove.stop="dragMove"
|
||||
></view>
|
||||
<view
|
||||
class="uni-cropper-point point-lt"
|
||||
data-drag="leftTop"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fixed bottom-8 left-0 w-full px-10">
|
||||
<button
|
||||
class="mt-4 w-full rounded-full bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg transition-all active:scale-95"
|
||||
@click="getImage"
|
||||
>
|
||||
选择头像
|
||||
</button>
|
||||
<button
|
||||
class="mt-4 w-full rounded-full bg-gradient-to-r from-red-500 to-red-600 text-white shadow-lg transition-all active:scale-95"
|
||||
@click="getImageInfo"
|
||||
>
|
||||
提交
|
||||
</button>
|
||||
</view>
|
||||
<canvas
|
||||
canvas-id="myCanvas"
|
||||
:style="
|
||||
'position:absolute;border: 1px solid red; width:' +
|
||||
imageW +
|
||||
'px;height:' +
|
||||
imageH +
|
||||
'px;top:-9999px;left:-9999px;'
|
||||
"
|
||||
></canvas>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from "@/config";
|
||||
import { useUserStore } from "@/store";
|
||||
import { uploadAvatar } from "@/api/system/user";
|
||||
|
||||
const baseUrl = config.baseUrl;
|
||||
let sysInfo = uni.getSystemInfoSync();
|
||||
let SCREEN_WIDTH = sysInfo.screenWidth;
|
||||
let PAGE_X, // 手按下的x位置
|
||||
PAGE_Y, // 手按下y的位置
|
||||
PR = sysInfo.pixelRatio, // dpi
|
||||
T_PAGE_X, // 手移动的时候x的位置
|
||||
T_PAGE_Y, // 手移动的时候Y的位置
|
||||
CUT_L, // 初始化拖拽元素的left值
|
||||
CUT_T, // 初始化拖拽元素的top值
|
||||
CUT_R, // 初始化拖拽元素的
|
||||
CUT_B, // 初始化拖拽元素的
|
||||
CUT_W, // 初始化拖拽元素的宽度
|
||||
CUT_H, // 初始化拖拽元素的高度
|
||||
IMG_RATIO, // 图片比例
|
||||
IMG_REAL_W, // 图片实际的宽度
|
||||
IMG_REAL_H, // 图片实际的高度
|
||||
DRAFG_MOVE_RATIO = 1, //移动时候的比例,
|
||||
INIT_DRAG_POSITION = 100, // 初始化屏幕宽度和裁剪区域的宽度之差,用于设置初始化裁剪的宽度
|
||||
DRAW_IMAGE_W = sysInfo.screenWidth; // 设置生成的图片宽度
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
imageSrc: useUserStore().avatar,
|
||||
isShowImg: false,
|
||||
// 初始化的宽高
|
||||
cropperInitW: SCREEN_WIDTH,
|
||||
cropperInitH: SCREEN_WIDTH,
|
||||
// 动态的宽高
|
||||
cropperW: SCREEN_WIDTH,
|
||||
cropperH: SCREEN_WIDTH,
|
||||
// 动态的left top值
|
||||
cropperL: 0,
|
||||
cropperT: 0,
|
||||
|
||||
transL: 0,
|
||||
transT: 0,
|
||||
|
||||
// 图片缩放值
|
||||
scaleP: 0,
|
||||
imageW: 0,
|
||||
imageH: 0,
|
||||
|
||||
// 裁剪框 宽高
|
||||
cutL: 0,
|
||||
cutT: 0,
|
||||
cutB: SCREEN_WIDTH,
|
||||
cutR: "100%",
|
||||
qualityWidth: DRAW_IMAGE_W,
|
||||
innerAspectRadio: DRAFG_MOVE_RATIO,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady: function () {
|
||||
this.loadImage();
|
||||
},
|
||||
methods: {
|
||||
setData: function (obj) {
|
||||
let that = this;
|
||||
Object.keys(obj).forEach(function (key) {
|
||||
that.$set(that.$data, key, obj[key]);
|
||||
});
|
||||
},
|
||||
getImage: function () {
|
||||
var _this = this;
|
||||
uni.chooseImage({
|
||||
success: function (res) {
|
||||
_this.setData({
|
||||
imageSrc: res.tempFilePaths[0],
|
||||
});
|
||||
_this.loadImage();
|
||||
},
|
||||
});
|
||||
},
|
||||
loadImage: function () {
|
||||
var _this = this;
|
||||
|
||||
uni.getImageInfo({
|
||||
src: _this.imageSrc,
|
||||
success: function success(res) {
|
||||
IMG_RATIO = 1 / 1;
|
||||
if (IMG_RATIO >= 1) {
|
||||
IMG_REAL_W = SCREEN_WIDTH;
|
||||
IMG_REAL_H = SCREEN_WIDTH / IMG_RATIO;
|
||||
} else {
|
||||
IMG_REAL_W = SCREEN_WIDTH * IMG_RATIO;
|
||||
IMG_REAL_H = SCREEN_WIDTH;
|
||||
}
|
||||
let minRange = IMG_REAL_W > IMG_REAL_H ? IMG_REAL_W : IMG_REAL_H;
|
||||
INIT_DRAG_POSITION =
|
||||
minRange > INIT_DRAG_POSITION ? INIT_DRAG_POSITION : minRange;
|
||||
// 根据图片的宽高显示不同的效果 保证图片可以正常显示
|
||||
if (IMG_RATIO >= 1) {
|
||||
let cutT = Math.ceil(
|
||||
(SCREEN_WIDTH / IMG_RATIO -
|
||||
(SCREEN_WIDTH / IMG_RATIO - INIT_DRAG_POSITION)) /
|
||||
2,
|
||||
);
|
||||
let cutB = cutT;
|
||||
let cutL = Math.ceil(
|
||||
(SCREEN_WIDTH - SCREEN_WIDTH + INIT_DRAG_POSITION) / 2,
|
||||
);
|
||||
let cutR = cutL;
|
||||
_this.setData({
|
||||
cropperW: SCREEN_WIDTH,
|
||||
cropperH: SCREEN_WIDTH / IMG_RATIO,
|
||||
// 初始化left right
|
||||
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
|
||||
cropperT: Math.ceil(
|
||||
(SCREEN_WIDTH - SCREEN_WIDTH / IMG_RATIO) / 2,
|
||||
),
|
||||
cutL: cutL,
|
||||
cutT: cutT,
|
||||
cutR: cutR,
|
||||
cutB: cutB,
|
||||
// 图片缩放值
|
||||
imageW: IMG_REAL_W,
|
||||
imageH: IMG_REAL_H,
|
||||
scaleP: IMG_REAL_W / SCREEN_WIDTH,
|
||||
qualityWidth: DRAW_IMAGE_W,
|
||||
innerAspectRadio: IMG_RATIO,
|
||||
});
|
||||
} else {
|
||||
let cutL = Math.ceil(
|
||||
(SCREEN_WIDTH * IMG_RATIO - SCREEN_WIDTH * IMG_RATIO) / 2,
|
||||
);
|
||||
let cutR = cutL;
|
||||
let cutT = Math.ceil((SCREEN_WIDTH - INIT_DRAG_POSITION) / 2);
|
||||
let cutB = cutT;
|
||||
_this.setData({
|
||||
cropperW: SCREEN_WIDTH * IMG_RATIO,
|
||||
cropperH: SCREEN_WIDTH,
|
||||
// 初始化left right
|
||||
cropperL: Math.ceil(
|
||||
(SCREEN_WIDTH - SCREEN_WIDTH * IMG_RATIO) / 2,
|
||||
),
|
||||
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
|
||||
|
||||
cutL: cutL,
|
||||
cutT: cutT,
|
||||
cutR: cutR,
|
||||
cutB: cutB,
|
||||
// 图片缩放值
|
||||
imageW: IMG_REAL_W,
|
||||
imageH: IMG_REAL_H,
|
||||
scaleP: IMG_REAL_W / SCREEN_WIDTH,
|
||||
qualityWidth: DRAW_IMAGE_W,
|
||||
innerAspectRadio: IMG_RATIO,
|
||||
});
|
||||
}
|
||||
_this.setData({
|
||||
isShowImg: true,
|
||||
});
|
||||
uni.hideLoading();
|
||||
},
|
||||
});
|
||||
},
|
||||
// 拖动时候触发的touchStart事件
|
||||
contentStartMove(e) {
|
||||
PAGE_X = e.touches[0].pageX;
|
||||
PAGE_Y = e.touches[0].pageY;
|
||||
},
|
||||
|
||||
// 拖动时候触发的touchMove事件
|
||||
contentMoveing(e) {
|
||||
var _this = this;
|
||||
var dragLengthX = (PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
|
||||
var dragLengthY = (PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
|
||||
// 左移
|
||||
if (dragLengthX > 0) {
|
||||
if (this.cutL - dragLengthX < 0) dragLengthX = this.cutL;
|
||||
} else {
|
||||
if (this.cutR + dragLengthX < 0) dragLengthX = -this.cutR;
|
||||
}
|
||||
|
||||
if (dragLengthY > 0) {
|
||||
if (this.cutT - dragLengthY < 0) dragLengthY = this.cutT;
|
||||
} else {
|
||||
if (this.cutB + dragLengthY < 0) dragLengthY = -this.cutB;
|
||||
}
|
||||
this.setData({
|
||||
cutL: this.cutL - dragLengthX,
|
||||
cutT: this.cutT - dragLengthY,
|
||||
cutR: this.cutR + dragLengthX,
|
||||
cutB: this.cutB + dragLengthY,
|
||||
});
|
||||
|
||||
PAGE_X = e.touches[0].pageX;
|
||||
PAGE_Y = e.touches[0].pageY;
|
||||
},
|
||||
|
||||
contentTouchEnd() {},
|
||||
|
||||
// 获取图片
|
||||
getImageInfo() {
|
||||
var _this = this;
|
||||
uni.showLoading({
|
||||
title: "图片生成中...",
|
||||
});
|
||||
// 将图片写入画布
|
||||
const ctx = uni.createCanvasContext("myCanvas");
|
||||
ctx.drawImage(_this.imageSrc, 0, 0, IMG_REAL_W, IMG_REAL_H);
|
||||
ctx.draw(true, () => {
|
||||
// 获取画布要裁剪的位置和宽度 均为百分比 * 画布中图片的宽度 保证了在微信小程序中裁剪的图片模糊 位置不对的问题 canvasT = (_this.cutT / _this.cropperH) * (_this.imageH / pixelRatio)
|
||||
var canvasW =
|
||||
((_this.cropperW - _this.cutL - _this.cutR) / _this.cropperW) *
|
||||
IMG_REAL_W;
|
||||
var canvasH =
|
||||
((_this.cropperH - _this.cutT - _this.cutB) / _this.cropperH) *
|
||||
IMG_REAL_H;
|
||||
var canvasL = (_this.cutL / _this.cropperW) * IMG_REAL_W;
|
||||
var canvasT = (_this.cutT / _this.cropperH) * IMG_REAL_H;
|
||||
uni.canvasToTempFilePath({
|
||||
x: canvasL,
|
||||
y: canvasT,
|
||||
width: canvasW,
|
||||
height: canvasH,
|
||||
destWidth: canvasW,
|
||||
destHeight: canvasH,
|
||||
quality: 0.5,
|
||||
canvasId: "myCanvas",
|
||||
success: function (res) {
|
||||
uni.hideLoading();
|
||||
let data = { name: "avatarfile", filePath: res.tempFilePath };
|
||||
uploadAvatar(data).then((response) => {
|
||||
useUserStore().SET_AVATAR(baseUrl + response.imgUrl);
|
||||
uni.showToast({ title: "修改成功", icon: "success" });
|
||||
uni.navigateBack();
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
// 设置大小的时候触发的touchStart事件
|
||||
dragStart(e) {
|
||||
T_PAGE_X = e.touches[0].pageX;
|
||||
T_PAGE_Y = e.touches[0].pageY;
|
||||
CUT_L = this.cutL;
|
||||
CUT_R = this.cutR;
|
||||
CUT_B = this.cutB;
|
||||
CUT_T = this.cutT;
|
||||
},
|
||||
|
||||
// 设置大小的时候触发的touchMove事件
|
||||
dragMove(e) {
|
||||
var _this = this;
|
||||
var dragType = e.target.dataset.drag;
|
||||
switch (dragType) {
|
||||
case "right":
|
||||
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
|
||||
if (CUT_R + dragLength < 0) dragLength = -CUT_R;
|
||||
this.setData({
|
||||
cutR: CUT_R + dragLength,
|
||||
});
|
||||
break;
|
||||
case "left":
|
||||
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
|
||||
if (CUT_L - dragLength < 0) dragLength = CUT_L;
|
||||
if (CUT_L - dragLength > this.cropperW - this.cutR)
|
||||
dragLength = CUT_L - (this.cropperW - this.cutR);
|
||||
this.setData({
|
||||
cutL: CUT_L - dragLength,
|
||||
});
|
||||
break;
|
||||
case "top":
|
||||
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
|
||||
if (CUT_T - dragLength < 0) dragLength = CUT_T;
|
||||
if (CUT_T - dragLength > this.cropperH - this.cutB)
|
||||
dragLength = CUT_T - (this.cropperH - this.cutB);
|
||||
this.setData({
|
||||
cutT: CUT_T - dragLength,
|
||||
});
|
||||
break;
|
||||
case "bottom":
|
||||
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
|
||||
if (CUT_B + dragLength < 0) dragLength = -CUT_B;
|
||||
this.setData({
|
||||
cutB: CUT_B + dragLength,
|
||||
});
|
||||
break;
|
||||
case "rightBottom":
|
||||
var dragLengthX = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
|
||||
var dragLengthY = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
|
||||
|
||||
if (CUT_B + dragLengthY < 0) dragLengthY = -CUT_B;
|
||||
if (CUT_R + dragLengthX < 0) dragLengthX = -CUT_R;
|
||||
let cutB = CUT_B + dragLengthY;
|
||||
let cutR = CUT_R + dragLengthX;
|
||||
|
||||
this.setData({
|
||||
cutB: cutB,
|
||||
cutR: cutR,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 横向虚线 */
|
||||
.uni-cropper-dashed-h {
|
||||
position: absolute;
|
||||
top: 33.33333333%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 33.33333333%;
|
||||
border-top: 1rpx dashed rgba(255, 255, 255, 0.5);
|
||||
border-bottom: 1rpx dashed rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 纵向虚线 */
|
||||
.uni-cropper-dashed-v {
|
||||
position: absolute;
|
||||
left: 33.33333333%;
|
||||
top: 0;
|
||||
width: 33.33333333%;
|
||||
height: 100%;
|
||||
border-left: 1rpx dashed rgba(255, 255, 255, 0.5);
|
||||
border-right: 1rpx dashed rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 四个方向的线 为了之后的拖动事件*/
|
||||
.uni-cropper-line-t {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: #69f;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1rpx;
|
||||
opacity: 0.1;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.uni-cropper-line-t::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0rpx;
|
||||
width: 100%;
|
||||
-webkit-transform: translate3d(0, -50%, 0);
|
||||
transform: translate3d(0, -50%, 0);
|
||||
bottom: 0;
|
||||
height: 41rpx;
|
||||
background: transparent;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.uni-cropper-line-r {
|
||||
position: absolute;
|
||||
display: block;
|
||||
background-color: #69f;
|
||||
top: 0;
|
||||
right: 0rpx;
|
||||
width: 1rpx;
|
||||
opacity: 0.1;
|
||||
height: 100%;
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.uni-cropper-line-r::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 41rpx;
|
||||
-webkit-transform: translate3d(-50%, 0, 0);
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.uni-cropper-line-b {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: #69f;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 1rpx;
|
||||
opacity: 0.1;
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.uni-cropper-line-b::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0rpx;
|
||||
width: 100%;
|
||||
-webkit-transform: translate3d(0, -50%, 0);
|
||||
transform: translate3d(0, -50%, 0);
|
||||
bottom: 0;
|
||||
height: 41rpx;
|
||||
background: transparent;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.uni-cropper-line-l {
|
||||
position: absolute;
|
||||
display: block;
|
||||
background-color: #69f;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1rpx;
|
||||
opacity: 0.1;
|
||||
height: 100%;
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.uni-cropper-line-l::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 41rpx;
|
||||
-webkit-transform: translate3d(-50%, 0, 0);
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.uni-cropper-point {
|
||||
width: 5rpx;
|
||||
height: 5rpx;
|
||||
background-color: #69f;
|
||||
opacity: 0.75;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.point-t {
|
||||
top: -3rpx;
|
||||
left: 50%;
|
||||
margin-left: -3rpx;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.point-tr {
|
||||
top: -3rpx;
|
||||
left: 100%;
|
||||
margin-left: -3rpx;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.point-r {
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
margin-left: -3rpx;
|
||||
margin-top: -3rpx;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.point-rb {
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
-webkit-transform: translate3d(-50%, -50%, 0);
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
cursor: n-resize;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background-color: #69f;
|
||||
position: absolute;
|
||||
z-index: 1112;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.point-b {
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
margin-left: -3rpx;
|
||||
margin-top: -3rpx;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.point-bl {
|
||||
left: 0%;
|
||||
top: 100%;
|
||||
margin-left: -3rpx;
|
||||
margin-top: -3rpx;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.point-l {
|
||||
left: 0%;
|
||||
top: 50%;
|
||||
margin-left: -3rpx;
|
||||
margin-top: -3rpx;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.point-lt {
|
||||
left: 0%;
|
||||
top: 0%;
|
||||
margin-left: -3rpx;
|
||||
margin-top: -3rpx;
|
||||
cursor: n-resize;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 p-4 pb-24">
|
||||
<view v-for="(item, findex) in list" :key="findex" class="mb-6">
|
||||
<view
|
||||
class="mb-3 ml-2 flex items-center text-base font-bold text-gray-800"
|
||||
>
|
||||
<view :class="['mr-2 text-lg text-blue-500', item.icon]"></view
|
||||
>{{ item.title }}
|
||||
</view>
|
||||
<view
|
||||
class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-100"
|
||||
>
|
||||
<view
|
||||
v-for="(child, zindex) in item.childList"
|
||||
:key="zindex"
|
||||
class="relative"
|
||||
hover-class="bg-gray-50"
|
||||
@click="handleText(child)"
|
||||
>
|
||||
<view class="p-4 text-sm text-gray-600">{{ child.title }}</view>
|
||||
<view
|
||||
class="h-px w-full bg-gray-100"
|
||||
v-if="zindex !== item.childList.length - 1"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance } from "vue";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
icon: "i-mdi-github",
|
||||
title: "RuoYi-FastAPI问题",
|
||||
childList: [
|
||||
{
|
||||
title: "RuoYi-FastAPI开源吗?",
|
||||
content: "开源",
|
||||
},
|
||||
{
|
||||
title: "RuoYi-FastAPI可以商用吗?",
|
||||
content: "可以",
|
||||
},
|
||||
{
|
||||
title: "RuoYi-FastAPI官网地址多少?",
|
||||
content: "https://vfadmin.insistence.tech",
|
||||
},
|
||||
{
|
||||
title: "RuoYi-FastAPI文档地址多少?",
|
||||
content: "https://vfadmin.insistence.tech",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: "i-mdi-help-circle-outline",
|
||||
title: "其他问题",
|
||||
childList: [
|
||||
{
|
||||
title: "如何退出登录?",
|
||||
content: "请点击[我的] - [应用设置] - [退出登录]即可退出登录",
|
||||
},
|
||||
{
|
||||
title: "如何修改用户头像?",
|
||||
content: "请点击[我的] - [选择头像] - [点击提交]即可更换用户头像",
|
||||
},
|
||||
{
|
||||
title: "如何修改登录密码?",
|
||||
content: "请点击[我的] - [应用设置] - [修改密码]即可修改登录密码",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function handleText(item) {
|
||||
proxy.$tab.navigateTo(
|
||||
`/pages/common/textview/index?title=${item.title}&content=${item.content}`,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<view
|
||||
class="relative overflow-hidden bg-gradient-to-r from-blue-500 to-indigo-600 pb-20 pt-16 text-white shadow-lg"
|
||||
>
|
||||
<view
|
||||
class="absolute -right-10 -top-10 size-64 rounded-full bg-white/10 blur-3xl"
|
||||
></view>
|
||||
<view
|
||||
class="absolute -bottom-10 -left-10 size-40 rounded-full bg-white/10 blur-2xl"
|
||||
></view>
|
||||
|
||||
<view class="relative z-10 flex items-center justify-between px-6">
|
||||
<view class="flex items-center space-x-4">
|
||||
<!-- Avatar -->
|
||||
<view
|
||||
class="relative overflow-hidden rounded-full border-4 border-white/30 bg-white/20 shadow-xl transition-transform active:scale-95"
|
||||
>
|
||||
<image
|
||||
v-if="avatar"
|
||||
@click="handleToAvatar"
|
||||
:src="avatar"
|
||||
class="size-20 object-cover"
|
||||
/>
|
||||
<view
|
||||
v-else
|
||||
class="flex size-20 items-center justify-center bg-white text-gray-400"
|
||||
>
|
||||
<view class="i-mdi-account text-5xl"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- User Info -->
|
||||
<view class="flex flex-col">
|
||||
<template v-if="name">
|
||||
<view
|
||||
class="text-xl font-bold tracking-wide"
|
||||
@click="handleToInfo"
|
||||
>
|
||||
{{ name }}
|
||||
</view>
|
||||
<view
|
||||
class="mt-1 flex items-center text-sm text-blue-100"
|
||||
@click="handleToInfo"
|
||||
>
|
||||
<text>查看个人信息</text>
|
||||
<view class="i-mdi-chevron-right ml-1 text-xs"></view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="text-xl font-bold" @click="handleToLogin">
|
||||
点击登录
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Content Section -->
|
||||
<view class="relative z-20 -mt-12 flex-1 px-4 overflow-y-auto">
|
||||
<!-- Quick Actions -->
|
||||
<view
|
||||
class="mb-4 flex items-center justify-between rounded-2xl bg-white p-4 shadow-lg shadow-gray-200/50"
|
||||
>
|
||||
<view
|
||||
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
|
||||
@click="handleJiaoLiuQun"
|
||||
>
|
||||
<view
|
||||
class="flex size-12 items-center justify-center rounded-full bg-pink-50 text-pink-500"
|
||||
>
|
||||
<view class="i-mdi-account-group text-2xl"></view>
|
||||
</view>
|
||||
<text class="text-xs font-medium text-gray-600">交流群</text>
|
||||
</view>
|
||||
<view
|
||||
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-12 items-center justify-center rounded-full bg-blue-50 text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-face-agent text-2xl"></view>
|
||||
</view>
|
||||
<text class="text-xs font-medium text-gray-600">在线客服</text>
|
||||
</view>
|
||||
<view
|
||||
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-12 items-center justify-center rounded-full bg-purple-50 text-purple-500"
|
||||
>
|
||||
<view class="i-mdi-forum text-2xl"></view>
|
||||
</view>
|
||||
<text class="text-xs font-medium text-gray-600">反馈社区</text>
|
||||
</view>
|
||||
<view
|
||||
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-12 items-center justify-center rounded-full bg-green-50 text-green-500"
|
||||
>
|
||||
<view class="i-mdi-thumb-up text-2xl"></view>
|
||||
</view>
|
||||
<text class="text-xs font-medium text-gray-600">点赞我们</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Menu List -->
|
||||
<view
|
||||
class="overflow-hidden rounded-2xl bg-white shadow-lg shadow-gray-200/50"
|
||||
>
|
||||
<view
|
||||
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
|
||||
@click="handleToEditInfo"
|
||||
>
|
||||
<view class="flex items-center space-x-3">
|
||||
<view class="i-mdi-account-edit text-xl text-blue-500"></view>
|
||||
<text class="text-base text-gray-700">编辑资料</text>
|
||||
</view>
|
||||
<view class="i-mdi-chevron-right text-gray-400"></view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
|
||||
@click="handleHelp"
|
||||
>
|
||||
<view class="flex items-center space-x-3">
|
||||
<view class="i-mdi-help-circle text-xl text-orange-500"></view>
|
||||
<text class="text-base text-gray-700">常见问题</text>
|
||||
</view>
|
||||
<view class="i-mdi-chevron-right text-gray-400"></view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
|
||||
@click="handleAbout"
|
||||
>
|
||||
<view class="flex items-center space-x-3">
|
||||
<view class="i-mdi-heart-outline text-xl text-red-500"></view>
|
||||
<text class="text-base text-gray-700">关于我们</text>
|
||||
</view>
|
||||
<view class="i-mdi-chevron-right text-gray-400"></view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="group flex items-center justify-between p-4 transition-colors active:bg-gray-50"
|
||||
@click="handleToSetting"
|
||||
>
|
||||
<view class="flex items-center space-x-3">
|
||||
<view class="i-mdi-cog-outline text-xl text-gray-500"></view>
|
||||
<text class="text-base text-gray-700">应用设置</text>
|
||||
</view>
|
||||
<view class="i-mdi-chevron-right text-gray-400"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from "@/store";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const name = computed(() => userStore.name);
|
||||
const avatar = computed(() => userStore.avatar);
|
||||
const windowHeight = computed(() => uni.getSystemInfoSync().windowHeight - 50);
|
||||
|
||||
function handleToInfo() {
|
||||
proxy.$tab.navigateTo("/pages/mine/info/index");
|
||||
}
|
||||
|
||||
function handleToEditInfo() {
|
||||
proxy.$tab.navigateTo("/pages/mine/info/edit");
|
||||
}
|
||||
|
||||
function handleToSetting() {
|
||||
proxy.$tab.navigateTo("/pages/mine/setting/index");
|
||||
}
|
||||
|
||||
function handleToLogin() {
|
||||
proxy.$tab.reLaunch("/pages/login");
|
||||
}
|
||||
|
||||
function handleToAvatar() {
|
||||
proxy.$tab.navigateTo("/pages/mine/avatar/index");
|
||||
}
|
||||
|
||||
function handleHelp() {
|
||||
proxy.$tab.navigateTo("/pages/mine/help/index");
|
||||
}
|
||||
|
||||
function handleAbout() {
|
||||
proxy.$tab.navigateTo("/pages/mine/about/index");
|
||||
}
|
||||
|
||||
function handleJiaoLiuQun() {
|
||||
proxy.$modal.showToast("模块建设中~");
|
||||
}
|
||||
|
||||
function handleBuilding() {
|
||||
proxy.$modal.showToast("模块建设中~");
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col overflow-y-auto bg-white p-4">
|
||||
<view class="space-y-5">
|
||||
<!-- Nickname -->
|
||||
<view class="group relative">
|
||||
<text class="mb-2 block text-sm font-medium text-gray-700"
|
||||
>用户昵称</text
|
||||
>
|
||||
<input
|
||||
v-model="user.nickName"
|
||||
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
|
||||
type="text"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Phone -->
|
||||
<view class="group relative">
|
||||
<text class="mb-2 block text-sm font-medium text-gray-700"
|
||||
>手机号码</text
|
||||
>
|
||||
<input
|
||||
v-model="user.phonenumber"
|
||||
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
|
||||
type="number"
|
||||
placeholder="请输入手机号码"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Email -->
|
||||
<view class="group relative">
|
||||
<text class="mb-2 block text-sm font-medium text-gray-700">邮箱</text>
|
||||
<input
|
||||
v-model="user.email"
|
||||
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
|
||||
type="text"
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Sex -->
|
||||
<view class="group relative">
|
||||
<text class="mb-2 block text-sm font-medium text-gray-700">性别</text>
|
||||
<view class="flex space-x-4">
|
||||
<view
|
||||
v-for="item in sexs"
|
||||
:key="item.value"
|
||||
@click="user.sex = item.value"
|
||||
class="flex flex-1 items-center justify-center rounded-xl border py-2.5 transition-all active:scale-95"
|
||||
:class="
|
||||
user.sex === item.value
|
||||
? 'bg-blue-50 border-blue-500 text-blue-600'
|
||||
: 'bg-gray-50 border-transparent text-gray-500'
|
||||
"
|
||||
>
|
||||
<text class="text-sm font-medium">{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<view class="pt-6">
|
||||
<button
|
||||
@click="submit"
|
||||
class="flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-all active:scale-95 active:bg-blue-600"
|
||||
>
|
||||
提 交
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getUserProfile, updateUserProfile } from "@/api/system/user";
|
||||
import { ref, getCurrentInstance } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const user = ref({
|
||||
nickName: "",
|
||||
phonenumber: "",
|
||||
email: "",
|
||||
sex: "",
|
||||
});
|
||||
const sexs = [
|
||||
{
|
||||
text: "男",
|
||||
value: "0",
|
||||
},
|
||||
{
|
||||
text: "女",
|
||||
value: "1",
|
||||
},
|
||||
];
|
||||
|
||||
function getUser() {
|
||||
getUserProfile().then((response) => {
|
||||
const { nickName, phonenumber, email, sex } = response.data;
|
||||
user.value = { nickName, phonenumber, email, sex };
|
||||
});
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!user.value.nickName) {
|
||||
proxy.$modal.msgError("用户昵称不能为空");
|
||||
return;
|
||||
}
|
||||
if (!user.value.phonenumber) {
|
||||
proxy.$modal.msgError("手机号码不能为空");
|
||||
return;
|
||||
}
|
||||
if (!/^1[3|4|5|6|7|8|9][0-9]\d{8}$/.test(user.value.phonenumber)) {
|
||||
proxy.$modal.msgError("请输入正确的手机号码");
|
||||
return;
|
||||
}
|
||||
if (!user.value.email) {
|
||||
proxy.$modal.msgError("邮箱地址不能为空");
|
||||
return;
|
||||
}
|
||||
if (!/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(user.value.email)) {
|
||||
proxy.$modal.msgError("请输入正确的邮箱地址");
|
||||
return;
|
||||
}
|
||||
|
||||
updateUserProfile(user.value).then((response) => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
setTimeout(() => {
|
||||
proxy.$tab.navigateBack();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
getUser();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 pt-4 pb-10">
|
||||
<view class="overflow-hidden bg-white shadow-sm">
|
||||
<!-- Nickname -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="flex size-9 items-center justify-center rounded-full bg-blue-50 text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-account text-xl"></view>
|
||||
</view>
|
||||
<text class="ml-3 text-base font-medium text-gray-800">昵称</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">{{ user.nickName }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Phone -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="flex size-9 items-center justify-center rounded-full bg-green-50 text-green-500"
|
||||
>
|
||||
<view class="i-mdi-phone text-xl"></view>
|
||||
</view>
|
||||
<text class="ml-3 text-base font-medium text-gray-800">手机号码</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">{{ user.phonenumber }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Email -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="flex size-9 items-center justify-center rounded-full bg-indigo-50 text-indigo-500"
|
||||
>
|
||||
<view class="i-mdi-email text-xl"></view>
|
||||
</view>
|
||||
<text class="ml-3 text-base font-medium text-gray-800">邮箱</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">{{ user.email }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Post -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="flex size-9 items-center justify-center rounded-full bg-orange-50 text-orange-500"
|
||||
>
|
||||
<view class="i-mdi-shield-account text-xl"></view>
|
||||
</view>
|
||||
<text class="ml-3 text-base font-medium text-gray-800">岗位</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">{{ postGroup }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Role -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="flex size-9 items-center justify-center rounded-full bg-purple-50 text-purple-500"
|
||||
>
|
||||
<view class="i-mdi-badge-account text-xl"></view>
|
||||
</view>
|
||||
<text class="ml-3 text-base font-medium text-gray-800">角色</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">{{ roleGroup }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Create Time -->
|
||||
<view
|
||||
class="flex items-center justify-between px-5 py-4 active:bg-gray-50"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="flex size-9 items-center justify-center rounded-full bg-pink-50 text-pink-500"
|
||||
>
|
||||
<view class="i-mdi-calendar text-xl"></view>
|
||||
</view>
|
||||
<text class="ml-3 text-base font-medium text-gray-800">创建日期</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500">{{ user.createTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getUserProfile } from "@/api/system/user";
|
||||
import { ref } from "vue";
|
||||
|
||||
const user = ref({});
|
||||
const roleGroup = ref("");
|
||||
const postGroup = ref("");
|
||||
|
||||
function getUser() {
|
||||
getUserProfile().then((response) => {
|
||||
user.value = response.data;
|
||||
roleGroup.value = response.roleGroup;
|
||||
postGroup.value = response.postGroup;
|
||||
});
|
||||
}
|
||||
|
||||
getUser();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view class="min-h-screen bg-white p-4">
|
||||
<view class="space-y-5">
|
||||
<!-- Old Password -->
|
||||
<view class="group relative">
|
||||
<text class="mb-2 block text-sm font-medium text-gray-700">旧密码</text>
|
||||
<input
|
||||
v-model="user.oldPassword"
|
||||
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
|
||||
type="password"
|
||||
placeholder="请输入旧密码"
|
||||
/>
|
||||
</view>
|
||||
<!-- New Password -->
|
||||
<view class="group relative">
|
||||
<text class="mb-2 block text-sm font-medium text-gray-700">新密码</text>
|
||||
<input
|
||||
v-model="user.newPassword"
|
||||
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</view>
|
||||
<!-- Confirm Password -->
|
||||
<view class="group relative">
|
||||
<text class="mb-2 block text-sm font-medium text-gray-700"
|
||||
>确认密码</text
|
||||
>
|
||||
<input
|
||||
v-model="user.confirmPassword"
|
||||
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
|
||||
type="password"
|
||||
placeholder="请确认新密码"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<view class="pt-6">
|
||||
<button
|
||||
@click="submit"
|
||||
class="flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-all active:scale-95 active:bg-blue-600"
|
||||
>
|
||||
提 交
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { updateUserPwd } from "@/api/system/user";
|
||||
import { ref, reactive, getCurrentInstance } from "vue";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const user = reactive({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
function submit() {
|
||||
if (!user.oldPassword) {
|
||||
proxy.$modal.msgError("旧密码不能为空");
|
||||
return;
|
||||
}
|
||||
if (!user.newPassword) {
|
||||
proxy.$modal.msgError("新密码不能为空");
|
||||
return;
|
||||
}
|
||||
if (user.newPassword.length < 6 || user.newPassword.length > 20) {
|
||||
proxy.$modal.msgError("长度在 6 到 20 个字符");
|
||||
return;
|
||||
}
|
||||
if (!user.confirmPassword) {
|
||||
proxy.$modal.msgError("确认密码不能为空");
|
||||
return;
|
||||
}
|
||||
if (user.newPassword !== user.confirmPassword) {
|
||||
proxy.$modal.msgError("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
|
||||
updateUserPwd(user.oldPassword, user.newPassword).then((response) => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
setTimeout(() => {
|
||||
proxy.$tab.navigateBack();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 pt-4 pb-10">
|
||||
<view class="bg-white">
|
||||
<!-- Change Password -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
|
||||
@click="handleToPwd"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view class="i-mdi-lock text-xl text-gray-600 mr-3"></view>
|
||||
<text class="text-base text-gray-800">修改密码</text>
|
||||
</view>
|
||||
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
|
||||
</view>
|
||||
|
||||
<!-- Check Update -->
|
||||
<view
|
||||
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
|
||||
@click="handleToUpgrade"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view class="i-mdi-refresh text-xl text-gray-600 mr-3"></view>
|
||||
<text class="text-base text-gray-800">检查更新</text>
|
||||
</view>
|
||||
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
|
||||
</view>
|
||||
|
||||
<!-- Clean Cache -->
|
||||
<view
|
||||
class="flex items-center justify-between px-5 py-4 active:bg-gray-50"
|
||||
@click="handleCleanTmp"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view class="i-mdi-delete text-xl text-gray-600 mr-3"></view>
|
||||
<text class="text-base text-gray-800">清理缓存</text>
|
||||
</view>
|
||||
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Logout -->
|
||||
<view class="mt-8 px-4">
|
||||
<view
|
||||
class="flex h-12 w-full items-center justify-center rounded-xl bg-red-50 text-base font-semibold text-red-600 transition-colors active:bg-red-100"
|
||||
@click="handleLogout"
|
||||
>退出登录</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from "@/store";
|
||||
import { getCurrentInstance } from "vue";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
function handleToPwd() {
|
||||
proxy.$tab.navigateTo("/pages/mine/pwd/index");
|
||||
}
|
||||
|
||||
function handleToUpgrade() {
|
||||
proxy.$modal.showToast("模块建设中~");
|
||||
}
|
||||
|
||||
function handleCleanTmp() {
|
||||
proxy.$modal.showToast("模块建设中~");
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
proxy.$modal.confirm("确定注销并退出系统吗?").then(() => {
|
||||
useUserStore()
|
||||
.logOut()
|
||||
.then(() => {})
|
||||
.finally(() => {
|
||||
proxy.$tab.reLaunch("/pages/index");
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<view
|
||||
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
|
||||
>
|
||||
<!-- Logo Section -->
|
||||
<view class="mb-6 flex flex-col items-center">
|
||||
<view
|
||||
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
|
||||
>
|
||||
<image
|
||||
class="size-10"
|
||||
:src="globalConfig.appInfo.logo"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-xl font-bold tracking-wide text-gray-800"
|
||||
>RuoYi-FastAPI移动端注册</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- Form Section -->
|
||||
<view class="w-full rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur-md">
|
||||
<!-- Username -->
|
||||
<view class="group relative mb-5">
|
||||
<view
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-account text-xl"></view>
|
||||
</view>
|
||||
<input
|
||||
v-model="registerForm.username"
|
||||
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
|
||||
type="text"
|
||||
placeholder="请输入账号"
|
||||
maxlength="30"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Password -->
|
||||
<view class="group relative mb-5">
|
||||
<view
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-lock text-xl"></view>
|
||||
</view>
|
||||
<input
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
|
||||
placeholder="请输入密码"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<view class="group relative mb-5">
|
||||
<view
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-lock text-xl"></view>
|
||||
</view>
|
||||
<input
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
|
||||
placeholder="请输入重复密码"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Captcha -->
|
||||
<view
|
||||
class="mb-8 flex items-center justify-between"
|
||||
v-if="captchaEnabled"
|
||||
>
|
||||
<view class="group relative mr-3 flex-1">
|
||||
<view
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
|
||||
>
|
||||
<view class="i-mdi-security text-xl"></view>
|
||||
</view>
|
||||
<input
|
||||
v-model="registerForm.code"
|
||||
type="number"
|
||||
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
|
||||
placeholder="验证码"
|
||||
maxlength="4"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="h-12 w-28 overflow-hidden rounded-xl bg-gray-100 shadow-sm transition-opacity active:opacity-80"
|
||||
@click="getCode"
|
||||
>
|
||||
<image :src="codeUrl" class="size-full object-cover"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Register Button -->
|
||||
<button
|
||||
@click="handleRegister"
|
||||
class="flex h-12 w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-transform active:scale-95"
|
||||
>
|
||||
注 册
|
||||
</button>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<view class="mt-6 flex flex-col items-center space-y-3">
|
||||
<view class="flex items-center text-sm text-gray-500">
|
||||
<text
|
||||
@click="handleUserLogin"
|
||||
class="ml-1 font-medium text-blue-600 active:opacity-70"
|
||||
>使用已有账号登录</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCodeImg, register } from "@/api/login";
|
||||
import { ref, getCurrentInstance } from "vue";
|
||||
import { useConfigStore } from "@/store";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const globalConfig = useConfigStore().config;
|
||||
const codeUrl = ref("");
|
||||
// 验证码开关
|
||||
const captchaEnabled = ref(true);
|
||||
const registerForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
code: "",
|
||||
uuid: "",
|
||||
});
|
||||
|
||||
// 用户登录
|
||||
function handleUserLogin() {
|
||||
proxy.$tab.navigateTo(`/pages/login`);
|
||||
}
|
||||
|
||||
// 获取图形验证码
|
||||
function getCode() {
|
||||
getCodeImg().then((res) => {
|
||||
captchaEnabled.value =
|
||||
res.captchaEnabled === undefined ? true : res.captchaEnabled;
|
||||
if (captchaEnabled.value) {
|
||||
codeUrl.value = "data:image/gif;base64," + res.img;
|
||||
registerForm.value.uuid = res.uuid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 注册方法
|
||||
async function handleRegister() {
|
||||
if (registerForm.value.username === "") {
|
||||
proxy.$modal.msgError("请输入您的账号");
|
||||
} else if (registerForm.value.password === "") {
|
||||
proxy.$modal.msgError("请输入您的密码");
|
||||
} else if (registerForm.value.confirmPassword === "") {
|
||||
proxy.$modal.msgError("请再次输入您的密码");
|
||||
} else if (
|
||||
registerForm.value.password !== registerForm.value.confirmPassword
|
||||
) {
|
||||
proxy.$modal.msgError("两次输入的密码不一致");
|
||||
} else if (registerForm.value.code === "" && captchaEnabled.value) {
|
||||
proxy.$modal.msgError("请输入验证码");
|
||||
} else {
|
||||
proxy.$modal.loading("注册中,请耐心等待...");
|
||||
userRegister();
|
||||
}
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
async function userRegister() {
|
||||
register(registerForm.value)
|
||||
.then((res) => {
|
||||
proxy.$modal.closeLoading();
|
||||
uni.showModal({
|
||||
title: "系统提示",
|
||||
content:
|
||||
"恭喜你,您的账号 " + registerForm.value.username + " 注册成功!",
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
uni.redirectTo({ url: `/pages/login` });
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
if (captchaEnabled.value) {
|
||||
getCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCode();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page {
|
||||
background-color: #ffffff;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
|
||||
<scroll-view scroll-y class="flex-1" :show-scrollbar="false">
|
||||
<view class="p-4 pb-24 space-y-6">
|
||||
<!-- Banner -->
|
||||
<view
|
||||
class="relative h-40 w-full overflow-hidden rounded-2xl shadow-lg shadow-indigo-100"
|
||||
>
|
||||
<swiper
|
||||
class="h-40 w-full"
|
||||
:current="swiperDotIndex"
|
||||
@change="changeSwiper"
|
||||
autoplay
|
||||
interval="3000"
|
||||
circular
|
||||
>
|
||||
<swiper-item v-for="(item, index) in data" :key="index">
|
||||
<view class="h-full w-full" @click="clickBannerItem(item)">
|
||||
<image
|
||||
:src="item.image"
|
||||
mode="scaleToFill"
|
||||
class="block h-full w-full"
|
||||
/>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<!-- Custom Dots -->
|
||||
<view
|
||||
class="absolute bottom-3 right-0 left-0 flex justify-center space-x-2 pointer-events-none"
|
||||
>
|
||||
<view
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:class="current === index ? 'bg-white w-4' : 'bg-white/50 w-1.5'"
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI Tools Section -->
|
||||
<view>
|
||||
<view class="mb-4 flex items-center space-x-2">
|
||||
<view class="h-4 w-1 rounded-full bg-indigo-500"></view>
|
||||
<text class="text-base font-bold text-gray-800">AI 生产力</text>
|
||||
</view>
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view
|
||||
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
|
||||
@click="handleToAiChat"
|
||||
>
|
||||
<view
|
||||
class="absolute -right-4 -top-4 size-20 rounded-full bg-indigo-50 opacity-50"
|
||||
></view>
|
||||
<view class="relative z-10 flex flex-col">
|
||||
<view
|
||||
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-indigo-500 text-white shadow-md shadow-indigo-200"
|
||||
>
|
||||
<view class="i-mdi-chat text-xl"></view>
|
||||
</view>
|
||||
<text class="font-bold text-gray-800">智能对话</text>
|
||||
<text class="mt-1 text-xs text-gray-500">智能助手</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="absolute -right-4 -top-4 size-20 rounded-full bg-purple-50 opacity-50"
|
||||
></view>
|
||||
<view class="relative z-10 flex flex-col">
|
||||
<view
|
||||
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-purple-500 text-white shadow-md shadow-purple-200"
|
||||
>
|
||||
<view class="i-mdi-image-multiple text-xl"></view>
|
||||
</view>
|
||||
<text class="font-bold text-gray-800">图像生成</text>
|
||||
<text class="mt-1 text-xs text-gray-500">创意工坊</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- System Management Section -->
|
||||
<view>
|
||||
<view class="mb-4 flex items-center space-x-2">
|
||||
<view class="h-4 w-1 rounded-full bg-blue-500"></view>
|
||||
<text class="text-base font-bold text-gray-800">系统管理</text>
|
||||
</view>
|
||||
<view class="rounded-2xl bg-white p-5 shadow-sm">
|
||||
<view class="flex flex-wrap justify-between gap-y-4">
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleToUserList"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-blue-50 text-blue-500 transition-colors group-active:bg-blue-100"
|
||||
>
|
||||
<view class="i-mdi-account text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600"
|
||||
>用户管理</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-orange-50 text-orange-500 transition-colors group-active:bg-orange-100"
|
||||
>
|
||||
<view class="i-mdi-badge-account text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600"
|
||||
>角色管理</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-green-50 text-green-500 transition-colors group-active:bg-green-100"
|
||||
>
|
||||
<view class="i-mdi-office-building text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600"
|
||||
>部门管理</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-red-50 text-red-500 transition-colors group-active:bg-red-100"
|
||||
>
|
||||
<view class="i-mdi-cog text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600"
|
||||
>配置管理</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-cyan-50 text-cyan-500 transition-colors group-active:bg-cyan-100"
|
||||
>
|
||||
<view class="i-mdi-book-open-page-variant text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600"
|
||||
>字典管理</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-yellow-50 text-yellow-500 transition-colors group-active:bg-yellow-100"
|
||||
>
|
||||
<view class="i-mdi-bullhorn text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600"
|
||||
>通知公告</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-pink-50 text-pink-500 transition-colors group-active:bg-pink-100"
|
||||
>
|
||||
<view class="i-mdi-file-document-outline text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600"
|
||||
>日志管理</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
|
||||
@click="handleBuilding"
|
||||
>
|
||||
<view
|
||||
class="flex size-10 items-center justify-center rounded-xl bg-gray-50 text-gray-500 transition-colors group-active:bg-gray-100"
|
||||
>
|
||||
<view class="i-mdi-dots-horizontal text-xl"></view>
|
||||
</view>
|
||||
<text class="text-[11px] font-medium text-gray-600">更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance } from "vue";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const current = ref(0);
|
||||
const swiperDotIndex = ref(0);
|
||||
const data = ref([
|
||||
{ image: "/static/images/banner/banner01.jpg" },
|
||||
{ image: "/static/images/banner/banner02.jpg" },
|
||||
{ image: "/static/images/banner/banner03.jpg" },
|
||||
]);
|
||||
|
||||
function clickBannerItem(item) {
|
||||
console.info(item);
|
||||
}
|
||||
|
||||
function changeSwiper(e) {
|
||||
current.value = e.detail.current;
|
||||
}
|
||||
|
||||
function handleToAiChat() {
|
||||
handleBuilding();
|
||||
}
|
||||
|
||||
function handleToUserList() {
|
||||
handleBuilding();
|
||||
}
|
||||
|
||||
function handleBuilding() {
|
||||
proxy.$modal.msg("模块建设中~");
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getToken } from "@/utils/auth";
|
||||
|
||||
// 登录页面
|
||||
const loginPage = "/pages/login";
|
||||
|
||||
// 页面白名单
|
||||
const whiteList = [
|
||||
"/pages/login",
|
||||
"/pages/register",
|
||||
"/pages/common/webview/index",
|
||||
"/pages/common/agreement/index",
|
||||
"/pages/common/privacy/index",
|
||||
];
|
||||
|
||||
// 检查地址白名单
|
||||
function checkWhite(url) {
|
||||
const path = url.split("?")[0];
|
||||
return whiteList.indexOf(path) !== -1;
|
||||
}
|
||||
|
||||
// 页面跳转验证拦截器
|
||||
let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
|
||||
list.forEach((item) => {
|
||||
uni.addInterceptor(item, {
|
||||
invoke(to) {
|
||||
if (getToken()) {
|
||||
if (to.url === loginPage) {
|
||||
uni.reLaunch({ url: "/" });
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
if (checkWhite(to.url)) {
|
||||
return true;
|
||||
}
|
||||
uni.reLaunch({ url: loginPage });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
console.log(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useUserStore } from "@/store";
|
||||
|
||||
function authPermission(permission) {
|
||||
const all_permission = "*:*:*";
|
||||
const permissions = useUserStore().permissions;
|
||||
if (permission && permission.length > 0) {
|
||||
return permissions.some((v) => {
|
||||
return all_permission === v || v === permission;
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function authRole(role) {
|
||||
const super_admin = "admin";
|
||||
const roles = useUserStore().roles;
|
||||
if (role && role.length > 0) {
|
||||
return roles.some((v) => {
|
||||
return super_admin === v || v === role;
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
// 验证用户是否具备某权限
|
||||
hasPermi(permission) {
|
||||
return authPermission(permission);
|
||||
},
|
||||
// 验证用户是否含有指定权限,只需包含其中一个
|
||||
hasPermiOr(permissions) {
|
||||
return permissions.some((item) => {
|
||||
return authPermission(item);
|
||||
});
|
||||
},
|
||||
// 验证用户是否含有指定权限,必须全部拥有
|
||||
hasPermiAnd(permissions) {
|
||||
return permissions.every((item) => {
|
||||
return authPermission(item);
|
||||
});
|
||||
},
|
||||
// 验证用户是否具备某角色
|
||||
hasRole(role) {
|
||||
return authRole(role);
|
||||
},
|
||||
// 验证用户是否含有指定角色,只需包含其中一个
|
||||
hasRoleOr(roles) {
|
||||
return roles.some((item) => {
|
||||
return authRole(item);
|
||||
});
|
||||
},
|
||||
// 验证用户是否含有指定角色,必须全部拥有
|
||||
hasRoleAnd(roles) {
|
||||
return roles.every((item) => {
|
||||
return authRole(item);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import tab from "./tab";
|
||||
import auth from "./auth";
|
||||
import modal from "./modal";
|
||||
|
||||
export function install(app) {
|
||||
// 页签操作
|
||||
app.config.globalProperties.$tab = tab;
|
||||
// 认证对象
|
||||
app.config.globalProperties.$auth = auth;
|
||||
// 模态框对象
|
||||
app.config.globalProperties.$modal = modal;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
export default {
|
||||
// 消息提示
|
||||
msg(content) {
|
||||
uni.showToast({
|
||||
title: content,
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
// 错误消息
|
||||
msgError(content) {
|
||||
uni.showToast({
|
||||
title: content,
|
||||
icon: "error",
|
||||
});
|
||||
},
|
||||
// 成功消息
|
||||
msgSuccess(content) {
|
||||
uni.showToast({
|
||||
title: content,
|
||||
icon: "success",
|
||||
});
|
||||
},
|
||||
// 隐藏消息
|
||||
hideMsg(content) {
|
||||
uni.hideToast();
|
||||
},
|
||||
// 弹出提示
|
||||
alert(content, title) {
|
||||
uni.showModal({
|
||||
title: title || "系统提示",
|
||||
content: content,
|
||||
showCancel: false,
|
||||
});
|
||||
},
|
||||
// 确认窗体
|
||||
confirm(content, title) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title: title || "系统提示",
|
||||
content: content,
|
||||
cancelText: "取消",
|
||||
confirmText: "确定",
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
resolve(res.confirm);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
// 提示信息
|
||||
showToast(option) {
|
||||
if (typeof option === "object") {
|
||||
uni.showToast(option);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: option,
|
||||
icon: "none",
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
},
|
||||
// 打开遮罩层
|
||||
loading(content) {
|
||||
uni.showLoading({
|
||||
title: content,
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
// 关闭遮罩层
|
||||
closeLoading() {
|
||||
try {
|
||||
uni.hideLoading();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
// 关闭所有页面,打开到应用内的某个页面
|
||||
reLaunch(url) {
|
||||
return uni.reLaunch({
|
||||
url: url,
|
||||
});
|
||||
},
|
||||
// 跳转到tabBar页面,并关闭其他所有非tabBar页面
|
||||
switchTab(url) {
|
||||
return uni.switchTab({
|
||||
url: url,
|
||||
});
|
||||
},
|
||||
// 关闭当前页面,跳转到应用内的某个页面
|
||||
redirectTo(url) {
|
||||
return uni.redirectTo({
|
||||
url: url,
|
||||
});
|
||||
},
|
||||
// 保留当前页面,跳转到应用内的某个页面
|
||||
navigateTo(url) {
|
||||
return uni.navigateTo({
|
||||
url: url,
|
||||
});
|
||||
},
|
||||
// 关闭当前页面,返回上一页面或多级页面
|
||||
navigateBack() {
|
||||
return uni.navigateBack();
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1,9 @@
|
||||
import { createPinia } from "pinia";
|
||||
import { useUserStore } from "./modules/user";
|
||||
import { useConfigStore } from "./modules/config";
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
export default pinia;
|
||||
|
||||
export { useUserStore, useConfigStore };
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
export const useConfigStore = defineStore("config", () => {
|
||||
const config = ref();
|
||||
const setConfig = (val) => {
|
||||
config.value = val;
|
||||
};
|
||||
return {
|
||||
config,
|
||||
setConfig,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const useDictStore = defineStore("dict", {
|
||||
state: () => ({
|
||||
dict: new Array(),
|
||||
}),
|
||||
actions: {
|
||||
// 获取字典
|
||||
getDict(_key) {
|
||||
if (_key == null && _key == "") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
for (let i = 0; i < this.dict.length; i++) {
|
||||
if (this.dict[i].key == _key) {
|
||||
return this.dict[i].value;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
// 设置字典
|
||||
setDict(_key, value) {
|
||||
if (_key !== null && _key !== "") {
|
||||
this.dict.push({
|
||||
key: _key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
// 删除字典
|
||||
removeDict(_key) {
|
||||
var bln = false;
|
||||
try {
|
||||
for (let i = 0; i < this.dict.length; i++) {
|
||||
if (this.dict[i].key == _key) {
|
||||
this.dict.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
bln = false;
|
||||
}
|
||||
return bln;
|
||||
},
|
||||
// 清空字典
|
||||
cleanDict() {
|
||||
this.dict = new Array();
|
||||
},
|
||||
// 初始字典
|
||||
initDict() {},
|
||||
},
|
||||
});
|
||||
|
||||
export default useDictStore;
|
||||
@@ -0,0 +1,125 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import config from "@/config";
|
||||
import storage from "@/utils/storage";
|
||||
import constant from "@/utils/constant";
|
||||
import { isHttp, isEmpty } from "@/utils/validate";
|
||||
import { getInfo, login, logout } from "@/api/login";
|
||||
import { getToken, removeToken, setToken } from "@/utils/auth";
|
||||
import defAva from "@/static/images/profile.jpg";
|
||||
|
||||
const baseUrl = config.baseUrl;
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
const token = ref(getToken());
|
||||
const id = ref(storage.get(constant.id));
|
||||
const name = ref(storage.get(constant.name));
|
||||
const avatar = ref(storage.get(constant.avatar));
|
||||
const roles = ref(storage.get(constant.roles));
|
||||
const permissions = ref(storage.get(constant.permissions));
|
||||
|
||||
const SET_TOKEN = (val) => {
|
||||
token.value = val;
|
||||
};
|
||||
const SET_ID = (val) => {
|
||||
id.value = val;
|
||||
storage.set(constant.id, val);
|
||||
};
|
||||
const SET_NAME = (val) => {
|
||||
name.value = val;
|
||||
storage.set(constant.name, val);
|
||||
};
|
||||
const SET_AVATAR = (val) => {
|
||||
avatar.value = val;
|
||||
storage.set(constant.avatar, val);
|
||||
};
|
||||
const SET_ROLES = (val) => {
|
||||
roles.value = val;
|
||||
storage.set(constant.roles, val);
|
||||
};
|
||||
const SET_PERMISSIONS = (val) => {
|
||||
permissions.value = val;
|
||||
storage.set(constant.permissions, val);
|
||||
};
|
||||
|
||||
// 登录
|
||||
const loginAction = (userInfo) => {
|
||||
const username = userInfo.username.trim();
|
||||
const password = userInfo.password;
|
||||
const code = userInfo.code;
|
||||
const uuid = userInfo.uuid;
|
||||
return new Promise((resolve, reject) => {
|
||||
login(username, password, code, uuid)
|
||||
.then((res) => {
|
||||
setToken(res.token);
|
||||
SET_TOKEN(res.token);
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
const getInfoAction = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInfo()
|
||||
.then((res) => {
|
||||
const user = res.user;
|
||||
let avatar = user.avatar || "";
|
||||
if (!isHttp(avatar)) {
|
||||
avatar = isEmpty(avatar) ? defAva : baseUrl + avatar;
|
||||
}
|
||||
const userid =
|
||||
isEmpty(user) || isEmpty(user.userId) ? "" : user.userId;
|
||||
const username =
|
||||
isEmpty(user) || isEmpty(user.userName) ? "" : user.userName;
|
||||
if (res.roles && res.roles.length > 0) {
|
||||
SET_ROLES(res.roles);
|
||||
SET_PERMISSIONS(res.permissions);
|
||||
} else {
|
||||
SET_ROLES(["ROLE_DEFAULT"]);
|
||||
}
|
||||
SET_ID(userid);
|
||||
SET_NAME(username);
|
||||
SET_AVATAR(avatar);
|
||||
resolve(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 退出系统
|
||||
const logOutAction = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
logout(token.value)
|
||||
.then(() => {
|
||||
SET_TOKEN("");
|
||||
SET_ROLES([]);
|
||||
SET_PERMISSIONS([]);
|
||||
removeToken();
|
||||
storage.clean();
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
id,
|
||||
name,
|
||||
avatar,
|
||||
roles,
|
||||
permissions,
|
||||
SET_AVATAR,
|
||||
login: loginAction,
|
||||
getInfo: getInfoAction,
|
||||
logOut: logOutAction,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"light": {
|
||||
"navBgColor": "#f6f6f6",
|
||||
"navTxtStyle": "black"
|
||||
},
|
||||
"dark": {
|
||||
"navBgColor": "#191919",
|
||||
"navTxtStyle": "white"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
const TokenKey = "App-Token";
|
||||
|
||||
export function getToken() {
|
||||
return uni.getStorageSync(TokenKey);
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
return uni.setStorageSync(TokenKey, token);
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return uni.removeStorageSync(TokenKey);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 显示消息提示框
|
||||
* @param content 提示的标题
|
||||
*/
|
||||
export function toast(content) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: content,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模态弹窗
|
||||
* @param content 提示的标题
|
||||
*/
|
||||
export function showConfirm(content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: content,
|
||||
cancelText: "取消",
|
||||
confirmText: "确定",
|
||||
success: function (res) {
|
||||
resolve(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数处理
|
||||
* @param params 参数
|
||||
*/
|
||||
export function tansParams(params) {
|
||||
let result = "";
|
||||
for (const propName of Object.keys(params)) {
|
||||
const value = params[propName];
|
||||
var part = encodeURIComponent(propName) + "=";
|
||||
if (value !== null && value !== "" && typeof value !== "undefined") {
|
||||
if (typeof value === "object") {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (
|
||||
value[key] !== null &&
|
||||
value[key] !== "" &&
|
||||
typeof value[key] !== "undefined"
|
||||
) {
|
||||
let params = propName + "[" + key + "]";
|
||||
var subPart = encodeURIComponent(params) + "=";
|
||||
result += subPart + encodeURIComponent(value[key]) + "&";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result += part + encodeURIComponent(value) + "&";
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const constant = {
|
||||
avatar: "user_avatar",
|
||||
id: "user_id",
|
||||
name: "user_name",
|
||||
roles: "user_roles",
|
||||
permissions: "user_permissions",
|
||||
};
|
||||
|
||||
export default constant;
|
||||
@@ -0,0 +1,30 @@
|
||||
import useDictStore from "@/store/modules/dict";
|
||||
import { getDicts } from "@/api/system/dict/data";
|
||||
import { ref, toRefs } from "vue";
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
*/
|
||||
export function useDict(...args) {
|
||||
const res = ref({});
|
||||
return (() => {
|
||||
args.forEach((dictType, index) => {
|
||||
res.value[dictType] = [];
|
||||
const dicts = useDictStore().getDict(dictType);
|
||||
if (dicts) {
|
||||
res.value[dictType] = dicts;
|
||||
} else {
|
||||
getDicts(dictType).then((resp) => {
|
||||
res.value[dictType] = resp.data.map((p) => ({
|
||||
label: p.dictLabel,
|
||||
value: p.dictValue,
|
||||
elTagType: p.listClass,
|
||||
elTagClass: p.cssClass,
|
||||
}));
|
||||
useDictStore().setDict(dictType, res.value[dictType]);
|
||||
});
|
||||
}
|
||||
});
|
||||
return toRefs(res.value);
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
401: "认证失败,无法访问系统资源",
|
||||
403: "当前操作没有权限",
|
||||
404: "访问资源不存在",
|
||||
default: "系统未知错误,请反馈给管理员",
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import store from "@/store";
|
||||
|
||||
/**
|
||||
* 字符权限校验
|
||||
* @param {Array} value 校验值
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function checkPermi(value) {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissions = store.getters && store.getters.permissions;
|
||||
const permissionDatas = value;
|
||||
const all_permission = "*:*:*";
|
||||
|
||||
const hasPermission = permissions.some((permission) => {
|
||||
return (
|
||||
all_permission === permission || permissionDatas.includes(permission)
|
||||
);
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error(
|
||||
`need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限校验
|
||||
* @param {Array} value 校验值
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function checkRole(value) {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const roles = store.getters && store.getters.roles;
|
||||
const permissionRoles = value;
|
||||
const super_admin = "admin";
|
||||
|
||||
const hasRole = roles.some((role) => {
|
||||
return super_admin === role || permissionRoles.includes(role);
|
||||
});
|
||||
|
||||
if (!hasRole) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error(`need roles! Like checkRole="['admin','editor']"`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import config from "@/config";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import errorCode from "@/utils/errorCode";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { toast, showConfirm, tansParams } from "@/utils/common";
|
||||
|
||||
let timeout = 10000;
|
||||
const baseUrl = config.baseUrl;
|
||||
|
||||
const request = (config) => {
|
||||
// 是否需要设置 token
|
||||
const isToken = (config.headers || {}).isToken === false;
|
||||
config.header = config.header || {};
|
||||
if (getToken() && !isToken) {
|
||||
config.header["Authorization"] = "Bearer " + getToken();
|
||||
}
|
||||
// get请求映射params参数
|
||||
if (config.params) {
|
||||
let url = config.url + "?" + tansParams(config.params);
|
||||
url = url.slice(0, -1);
|
||||
config.url = url;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni
|
||||
.request({
|
||||
method: config.method || "get",
|
||||
timeout: config.timeout || timeout,
|
||||
url: config.baseUrl || baseUrl + config.url,
|
||||
data: config.data,
|
||||
header: config.header,
|
||||
dataType: "json",
|
||||
})
|
||||
.then((response) => {
|
||||
const res = response;
|
||||
const code = res.data.code || 200;
|
||||
const msg = errorCode[code] || res.data.msg || errorCode["default"];
|
||||
if (code === 401) {
|
||||
showConfirm(
|
||||
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
|
||||
).then((res) => {
|
||||
if (res.confirm) {
|
||||
useUserStore()
|
||||
.logOut()
|
||||
.then((res) => {
|
||||
uni.reLaunch({ url: "/pages/login" });
|
||||
});
|
||||
}
|
||||
});
|
||||
reject("无效的会话,或者会话已过期,请重新登录。");
|
||||
} else if (code === 500) {
|
||||
toast(msg);
|
||||
reject("500");
|
||||
} else if (code !== 200) {
|
||||
toast(msg);
|
||||
reject(code);
|
||||
}
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
let { message } = error;
|
||||
if (message === "Network Error") {
|
||||
message = "后端接口连接异常";
|
||||
} else if (message.includes("timeout")) {
|
||||
message = "系统接口请求超时";
|
||||
} else if (message.includes("Request failed with status code")) {
|
||||
message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||
}
|
||||
toast(message);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default request;
|
||||
@@ -0,0 +1,38 @@
|
||||
import constant from "./constant";
|
||||
|
||||
// 存储变量名
|
||||
let storageKey = "storage_data";
|
||||
|
||||
// 存储节点变量名
|
||||
let storageNodeKeys = [
|
||||
constant.avatar,
|
||||
constant.id,
|
||||
constant.name,
|
||||
constant.roles,
|
||||
constant.permissions,
|
||||
];
|
||||
|
||||
const storage = {
|
||||
set: function (key, value) {
|
||||
if (storageNodeKeys.indexOf(key) != -1) {
|
||||
let tmp = uni.getStorageSync(storageKey);
|
||||
tmp = tmp ? tmp : {};
|
||||
tmp[key] = value;
|
||||
uni.setStorageSync(storageKey, tmp);
|
||||
}
|
||||
},
|
||||
get: function (key) {
|
||||
let storageData = uni.getStorageSync(storageKey) || {};
|
||||
return storageData[key] || "";
|
||||
},
|
||||
remove: function (key) {
|
||||
let storageData = uni.getStorageSync(storageKey) || {};
|
||||
delete storageData[key];
|
||||
uni.setStorageSync(storageKey, storageData);
|
||||
},
|
||||
clean: function () {
|
||||
uni.removeStorageSync(storageKey);
|
||||
},
|
||||
};
|
||||
|
||||
export default storage;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useUserStore } from "@/store";
|
||||
import config from "@/config";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import errorCode from "@/utils/errorCode";
|
||||
import { toast, showConfirm, tansParams } from "@/utils/common";
|
||||
|
||||
let timeout = 10000;
|
||||
const baseUrl = config.baseUrl;
|
||||
|
||||
export default function upload(config) {
|
||||
// 是否需要设置 token
|
||||
const isToken = (config.headers || {}).isToken === false;
|
||||
config.header = config.header || {};
|
||||
if (getToken() && !isToken) {
|
||||
config.header["Authorization"] = "Bearer " + getToken();
|
||||
}
|
||||
// get请求映射params参数
|
||||
if (config.params) {
|
||||
let url = config.url + "?" + tansParams(config.params);
|
||||
url = url.slice(0, -1);
|
||||
config.url = url;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
timeout: config.timeout || timeout,
|
||||
url: baseUrl + config.url,
|
||||
filePath: config.filePath,
|
||||
name: config.name || "file",
|
||||
header: config.header,
|
||||
formData: config.formData,
|
||||
success: (res) => {
|
||||
let result = JSON.parse(res.data);
|
||||
const code = result.code || 200;
|
||||
const msg = errorCode[code] || result.msg || errorCode["default"];
|
||||
if (code === 200) {
|
||||
resolve(result);
|
||||
} else if (code == 401) {
|
||||
showConfirm(
|
||||
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
|
||||
).then((res) => {
|
||||
if (res.confirm) {
|
||||
useUserStore()
|
||||
.logOut()
|
||||
.then((res) => {
|
||||
uni.reLaunch({ url: "/pages/login/login" });
|
||||
});
|
||||
}
|
||||
});
|
||||
reject("无效的会话,或者会话已过期,请重新登录。");
|
||||
} else if (code === 500) {
|
||||
toast(msg);
|
||||
reject("500");
|
||||
} else if (code !== 200) {
|
||||
toast(msg);
|
||||
reject(code);
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
let { message } = error;
|
||||
if (message == "Network Error") {
|
||||
message = "后端接口连接异常";
|
||||
} else if (message.includes("timeout")) {
|
||||
message = "系统接口请求超时";
|
||||
} else if (message.includes("Request failed with status code")) {
|
||||
message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||
}
|
||||
toast(message);
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 路径匹配器
|
||||
* @param {string} pattern
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isPathMatch(pattern, path) {
|
||||
const regexPattern = pattern
|
||||
.replace(/\//g, "\\/")
|
||||
.replace(/\*\*/g, ".*")
|
||||
.replace(/\*/g, "[^\\/]*");
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断value字符串是否为空
|
||||
* @param {string} value
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isEmpty(value) {
|
||||
if (
|
||||
value == null ||
|
||||
value == "" ||
|
||||
value == undefined ||
|
||||
value == "undefined"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断url是否是http或https
|
||||
* @param {string} url
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isHttp(url) {
|
||||
return url.indexOf("http://") !== -1 || url.indexOf("https://") !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断path是否为外链
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validUsername(str) {
|
||||
const valid_map = ["admin", "editor"];
|
||||
return valid_map.indexOf(str.trim()) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validURL(url) {
|
||||
const reg =
|
||||
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
|
||||
return reg.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validLowerCase(str) {
|
||||
const reg = /^[a-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validUpperCase(str) {
|
||||
const reg = /^[A-Z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validAlphabets(str) {
|
||||
const reg = /^[A-Za-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validEmail(email) {
|
||||
const reg =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return reg.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isString(str) {
|
||||
return typeof str === "string" || str instanceof String;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} arg
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isArray(arg) {
|
||||
if (typeof Array.isArray === "undefined") {
|
||||
return Object.prototype.toString.call(arg) === "[object Array]";
|
||||
}
|
||||
return Array.isArray(arg);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { icebreaker } from "@icebreakers/stylelint-config";
|
||||
|
||||
export default icebreaker();
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import { getIconCollections, iconsPlugin } from "@egoist/tailwindcss-icons";
|
||||
import cssMacro from "weapp-tailwindcss/css-macro";
|
||||
import { isMp } from "./platform";
|
||||
|
||||
export default <Config>{
|
||||
content: ["./index.html", "./src/**/*.{html,js,ts,jsx,tsx,vue}"],
|
||||
theme: {
|
||||
extend: {
|
||||
// colors: {
|
||||
// // 你可以在这里进行颜色的扩展
|
||||
// primary: {
|
||||
// 'DEFAULT': 'var(--color-primary, #0089FF)',
|
||||
// 'light-3': 'var(--color-primary-light-3, rgb(85, 199, 255))',
|
||||
// 'light-5': 'var(--color-primary-light-5, rgb(130, 217, 255))',
|
||||
// 'light-7': 'var(--color-primary-light-7, rgb(175, 235, 255))',
|
||||
// 'light-9': 'var(--color-primary-light-9, rgb(219, 252, 255))',
|
||||
// 'dark-2': 'var(--color-primary-dark-2, rgb(0, 135, 204))',
|
||||
// },
|
||||
// },
|
||||
},
|
||||
},
|
||||
// https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro
|
||||
plugins: [
|
||||
cssMacro({
|
||||
variantsMap: {
|
||||
wx: "MP-WEIXIN",
|
||||
"-wx": {
|
||||
value: "MP-WEIXIN",
|
||||
negative: true,
|
||||
},
|
||||
// 定义多个条件判断
|
||||
// mv: {
|
||||
// value: 'H5 || MP-WEIXIN'
|
||||
// },
|
||||
// '-mv': {
|
||||
// value: 'H5 || MP-WEIXIN',
|
||||
// negative: true
|
||||
// }
|
||||
},
|
||||
}),
|
||||
iconsPlugin({
|
||||
// 在这里可以选择你要使用的 icon, 更多详见:
|
||||
// https://icon-sets.iconify.design/
|
||||
collections: getIconCollections(["svg-spinners", "mdi"]),
|
||||
}),
|
||||
],
|
||||
corePlugins: {
|
||||
// 小程序去使用 h5 的 preflight 和响应式 container 没有意义
|
||||
preflight: !isMp,
|
||||
container: !isMp,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"outDir": "true",
|
||||
"jsx": "preserve",
|
||||
"lib": ["esnext", "dom"],
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": ["@dcloudio/types"],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import uni from "@dcloudio/vite-plugin-uni";
|
||||
import { defineConfig } from "vite";
|
||||
import { UnifiedViteWeappTailwindcssPlugin } from "weapp-tailwindcss/vite";
|
||||
import { WeappTailwindcssDisabled } from "./platform";
|
||||
import postcssPlugins from "./postcss.config";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => {
|
||||
// 新版本的 unplugin-auto-import 改成了只有 esm 格式的产物,而 uni-app 目前必须 cjs 格式
|
||||
// 所以需要改成动态 import 的写法来进行引入
|
||||
// 详见 https://github.com/sonofmagic/uni-app-vite-vue3-tailwind-vscode-template/issues/29
|
||||
const { default: AutoImport } = await import("unplugin-auto-import/vite");
|
||||
return {
|
||||
// uvtw 一定要放在 uni 后面
|
||||
plugins: [
|
||||
uni(),
|
||||
UnifiedViteWeappTailwindcssPlugin({
|
||||
rem2rpx: true,
|
||||
disabled: WeappTailwindcssDisabled,
|
||||
}),
|
||||
AutoImport({
|
||||
imports: ["vue", "uni-app", "pinia"],
|
||||
dts: "./src/auto-imports.d.ts",
|
||||
}),
|
||||
],
|
||||
// 内联 postcss 注册 tailwindcss
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: postcssPlugins,
|
||||
},
|
||||
// https://vitejs.dev/config/shared-options.html#css-preprocessoroptions
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ["legacy-js-api"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
|
||||
# 应用端口
|
||||
APP_PORT = 9099
|
||||
# 应用版本
|
||||
APP_VERSION= '1.1.2'
|
||||
APP_VERSION= '1.9.0'
|
||||
# 应用是否开启热重载
|
||||
APP_RELOAD = true
|
||||
# 应用工作进程数
|
||||
APP_WORKERS = 1
|
||||
# 应用是否开启IP归属区域查询
|
||||
APP_IP_LOCATION_QUERY = true
|
||||
# 应用是否允许账号同时登录
|
||||
APP_SAME_TIME_LOGIN = true
|
||||
# 应用是否为演示模式
|
||||
APP_DEMO_MODE = false
|
||||
# 应用是否禁用Swagger文档
|
||||
APP_DISABLE_SWAGGER = false
|
||||
# 应用是否禁用ReDoc文档
|
||||
APP_DISABLE_REDOC = false
|
||||
|
||||
# -------- Jwt配置 --------
|
||||
# Jwt秘钥
|
||||
@@ -30,6 +38,8 @@ JWT_REDIS_EXPIRE_MINUTES = 30
|
||||
|
||||
|
||||
# -------- 数据库配置 --------
|
||||
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
|
||||
DB_TYPE = 'mysql'
|
||||
# 数据库主机
|
||||
DB_HOST = '127.0.0.1'
|
||||
# 数据库端口
|
||||
@@ -62,3 +72,49 @@ REDIS_USERNAME = ''
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'dev'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# -------- 应用配置 --------
|
||||
# 应用运行环境
|
||||
APP_ENV = 'prod'
|
||||
# 应用名称
|
||||
APP_NAME = 'RuoYi-FastAPI'
|
||||
# 应用代理路径
|
||||
APP_ROOT_PATH = '/docker-api'
|
||||
# 应用主机
|
||||
APP_HOST = '0.0.0.0'
|
||||
# 应用端口
|
||||
APP_PORT = 9099
|
||||
# 应用版本
|
||||
APP_VERSION= '1.9.0'
|
||||
# 应用是否开启热重载
|
||||
APP_RELOAD = false
|
||||
# 应用工作进程数
|
||||
APP_WORKERS = 1
|
||||
# 应用是否开启IP归属区域查询
|
||||
APP_IP_LOCATION_QUERY = true
|
||||
# 应用是否允许账号同时登录
|
||||
APP_SAME_TIME_LOGIN = true
|
||||
# 应用是否为演示模式
|
||||
APP_DEMO_MODE = false
|
||||
# 应用是否禁用Swagger文档
|
||||
APP_DISABLE_SWAGGER = true
|
||||
# 应用是否禁用ReDoc文档
|
||||
APP_DISABLE_REDOC = true
|
||||
|
||||
# -------- Jwt配置 --------
|
||||
# Jwt秘钥
|
||||
JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55'
|
||||
# Jwt算法
|
||||
JWT_ALGORITHM = 'HS256'
|
||||
# 令牌过期时间
|
||||
JWT_EXPIRE_MINUTES = 1440
|
||||
# redis中令牌过期时间
|
||||
JWT_REDIS_EXPIRE_MINUTES = 30
|
||||
|
||||
|
||||
# -------- 数据库配置 --------
|
||||
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
|
||||
DB_TYPE = 'mysql'
|
||||
# 数据库主机
|
||||
DB_HOST = 'ruoyi-mysql'
|
||||
# 数据库端口
|
||||
DB_PORT = 3306
|
||||
# 数据库用户名
|
||||
DB_USERNAME = 'root'
|
||||
# 数据库密码
|
||||
DB_PASSWORD = 'root'
|
||||
# 数据库名称
|
||||
DB_DATABASE = 'ruoyi-fastapi'
|
||||
# 是否开启sqlalchemy日志
|
||||
DB_ECHO = true
|
||||
# 允许溢出连接池大小的最大连接数
|
||||
DB_MAX_OVERFLOW = 10
|
||||
# 连接池大小,0表示连接数无限制
|
||||
DB_POOL_SIZE = 50
|
||||
# 连接回收时间(单位:秒)
|
||||
DB_POOL_RECYCLE = 3600
|
||||
# 连接池中没有线程可用时,最多等待的时间(单位:秒)
|
||||
DB_POOL_TIMEOUT = 30
|
||||
|
||||
# -------- Redis配置 --------
|
||||
# Redis主机
|
||||
REDIS_HOST = 'ruoyi-redis'
|
||||
# Redis端口
|
||||
REDIS_PORT = 6379
|
||||
# Redis用户名
|
||||
REDIS_USERNAME = ''
|
||||
# Redis密码
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'dockermy'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
@@ -0,0 +1,120 @@
|
||||
# -------- 应用配置 --------
|
||||
# 应用运行环境
|
||||
APP_ENV = 'prod'
|
||||
# 应用名称
|
||||
APP_NAME = 'RuoYi-FastAPI'
|
||||
# 应用代理路径
|
||||
APP_ROOT_PATH = '/docker-api'
|
||||
# 应用主机
|
||||
APP_HOST = '0.0.0.0'
|
||||
# 应用端口
|
||||
APP_PORT = 9099
|
||||
# 应用版本
|
||||
APP_VERSION= '1.9.0'
|
||||
# 应用是否开启热重载
|
||||
APP_RELOAD = false
|
||||
# 应用工作进程数
|
||||
APP_WORKERS = 1
|
||||
# 应用是否开启IP归属区域查询
|
||||
APP_IP_LOCATION_QUERY = true
|
||||
# 应用是否为演示模式
|
||||
APP_DEMO_MODE = false
|
||||
# 应用是否允许账号同时登录
|
||||
APP_SAME_TIME_LOGIN = true
|
||||
# 应用是否禁用Swagger文档
|
||||
APP_DISABLE_SWAGGER = true
|
||||
# 应用是否禁用ReDoc文档
|
||||
APP_DISABLE_REDOC = true
|
||||
|
||||
# -------- Jwt配置 --------
|
||||
# Jwt秘钥
|
||||
JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55'
|
||||
# Jwt算法
|
||||
JWT_ALGORITHM = 'HS256'
|
||||
# 令牌过期时间
|
||||
JWT_EXPIRE_MINUTES = 1440
|
||||
# redis中令牌过期时间
|
||||
JWT_REDIS_EXPIRE_MINUTES = 30
|
||||
|
||||
|
||||
# -------- 数据库配置 --------
|
||||
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
|
||||
DB_TYPE = 'postgresql'
|
||||
# 数据库主机
|
||||
DB_HOST = 'ruoyi-pg'
|
||||
# 数据库端口
|
||||
DB_PORT = 5432
|
||||
# 数据库用户名
|
||||
DB_USERNAME = 'postgres'
|
||||
# 数据库密码
|
||||
DB_PASSWORD = 'root'
|
||||
# 数据库名称
|
||||
DB_DATABASE = 'ruoyi-fastapi'
|
||||
# 是否开启sqlalchemy日志
|
||||
DB_ECHO = true
|
||||
# 允许溢出连接池大小的最大连接数
|
||||
DB_MAX_OVERFLOW = 10
|
||||
# 连接池大小,0表示连接数无限制
|
||||
DB_POOL_SIZE = 50
|
||||
# 连接回收时间(单位:秒)
|
||||
DB_POOL_RECYCLE = 3600
|
||||
# 连接池中没有线程可用时,最多等待的时间(单位:秒)
|
||||
DB_POOL_TIMEOUT = 30
|
||||
|
||||
# -------- Redis配置 --------
|
||||
# Redis主机
|
||||
REDIS_HOST = 'ruoyi-redis'
|
||||
# Redis端口
|
||||
REDIS_PORT = 6379
|
||||
# Redis用户名
|
||||
REDIS_USERNAME = ''
|
||||
# Redis密码
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'dockerpg'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
|
||||
# 应用端口
|
||||
APP_PORT = 9099
|
||||
# 应用版本
|
||||
APP_VERSION= '1.1.2'
|
||||
APP_VERSION= '1.9.0'
|
||||
# 应用是否开启热重载
|
||||
APP_RELOAD = false
|
||||
# 应用工作进程数
|
||||
APP_WORKERS = 1
|
||||
# 应用是否开启IP归属区域查询
|
||||
APP_IP_LOCATION_QUERY = true
|
||||
# 应用是否允许账号同时登录
|
||||
APP_SAMETIME_LOGIN = true
|
||||
APP_SAME_TIME_LOGIN = true
|
||||
# 应用是否为演示模式
|
||||
APP_DEMO_MODE = false
|
||||
# 应用是否禁用Swagger文档
|
||||
APP_DISABLE_SWAGGER = true
|
||||
# 应用是否禁用ReDoc文档
|
||||
APP_DISABLE_REDOC = true
|
||||
|
||||
# -------- Jwt配置 --------
|
||||
# Jwt秘钥
|
||||
@@ -30,6 +38,8 @@ JWT_REDIS_EXPIRE_MINUTES = 30
|
||||
|
||||
|
||||
# -------- 数据库配置 --------
|
||||
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
|
||||
DB_TYPE = 'mysql'
|
||||
# 数据库主机
|
||||
DB_HOST = '127.0.0.1'
|
||||
# 数据库端口
|
||||
@@ -62,3 +72,49 @@ REDIS_USERNAME = ''
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'prod'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3.10
|
||||
WORKDIR /app
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 安装依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 9099
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "app.py", "--env=dockermy"]
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3.10
|
||||
WORKDIR /app
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 安装依赖
|
||||
RUN pip install --no-cache-dir -r requirements-pg.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 9099
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "app.py", "--env=dockerpg"]
|
||||
@@ -0,0 +1,153 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = %(here)s/alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
|
||||
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
|
||||
[settings]
|
||||
# This section is used to set environment variables for the alembic.
|
||||
# The env option is used to specify the environment for the alembic.
|
||||
# It can be set to dev or prod.
|
||||
env = dev
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration with an async dbapi.
|
||||
@@ -0,0 +1,135 @@
|
||||
import asyncio
|
||||
import os
|
||||
from collections.abc import Iterable, MutableMapping
|
||||
from logging.config import fileConfig
|
||||
from typing import Literal
|
||||
|
||||
from alembic import context
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations.ops import MigrationScript
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from config.database import ASYNC_SQLALCHEMY_DATABASE_URL, Base
|
||||
from utils.import_util import ImportUtil
|
||||
|
||||
# 判断vesrions目录是否存在,如果不存在则创建
|
||||
alembic_veresions_path = 'alembic/versions'
|
||||
if not os.path.exists(alembic_veresions_path):
|
||||
os.makedirs(alembic_veresions_path)
|
||||
|
||||
|
||||
# 自动查找所有模型
|
||||
found_models = ImportUtil.find_models(Base)
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
alembic_config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if alembic_config.config_file_name is not None:
|
||||
fileConfig(alembic_config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
target_metadata = Base.metadata
|
||||
# ASYNC_SQLALCHEMY_DATABASE_URL = 'mysql+asyncmy://root:mysqlroot@127.0.0.1:3306/ruoyi-fastapi'
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
alembic_config.set_main_option('sqlalchemy.url', ASYNC_SQLALCHEMY_DATABASE_URL)
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = alembic_config.get_main_option('sqlalchemy.url')
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={'paramstyle': 'named'},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
def include_name(
|
||||
name: str | None,
|
||||
type_: Literal['schema', 'table', 'column', 'index', 'unique_constraint', 'foreign_key_constraint'],
|
||||
parent_names: MutableMapping[Literal['schema_name', 'table_name', 'schema_qualified_table_name'], str | None],
|
||||
) -> bool:
|
||||
if type_ == 'table':
|
||||
return name in target_metadata.tables
|
||||
return True
|
||||
|
||||
def process_revision_directives(
|
||||
context: MigrationContext,
|
||||
revision: str | Iterable[str | None] | Iterable[str],
|
||||
directives: list[MigrationScript],
|
||||
) -> None:
|
||||
script = directives[0]
|
||||
|
||||
# 检查所有操作集是否为空
|
||||
all_empty = all(ops.is_empty() for ops in script.upgrade_ops_list)
|
||||
|
||||
if all_empty:
|
||||
# 如果没有实际变更,不生成迁移文件
|
||||
directives[:] = []
|
||||
print('❎️ 未检测到模型变更,不生成迁移文件')
|
||||
else:
|
||||
print('✅️ 检测到模型变更,生成迁移文件')
|
||||
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
transaction_per_migration=True,
|
||||
include_name=include_name,
|
||||
process_revision_directives=process_revision_directives,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
"""In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
connectable = async_engine_from_config(
|
||||
alembic_config.get_section(alembic_config.config_ini_section, {}),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode."""
|
||||
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -1,12 +1,18 @@
|
||||
import uvicorn
|
||||
from server import app, AppConfig
|
||||
|
||||
from config.env import AppConfig
|
||||
from server import create_app
|
||||
|
||||
if __name__ != '__main__':
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(
|
||||
app='app:app',
|
||||
app='server:create_app',
|
||||
host=AppConfig.app_host,
|
||||
port=AppConfig.app_port,
|
||||
root_path=AppConfig.app_root_path,
|
||||
reload=AppConfig.app_reload
|
||||
reload=AppConfig.app_reload,
|
||||
workers=AppConfig.app_workers,
|
||||
factory=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
import inspect
|
||||
import json
|
||||
import time
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from typing import Any, Literal, TypeVar
|
||||
|
||||
import httpx
|
||||
from async_lru import alru_cache
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse
|
||||
from starlette.status import HTTP_200_OK
|
||||
from typing_extensions import ParamSpec
|
||||
from user_agents import parse
|
||||
|
||||
from common.context import RequestContext
|
||||
from common.enums import BusinessType
|
||||
from config.env import AppConfig
|
||||
from exceptions.exception import LoginException, ServiceException, ServiceWarning
|
||||
from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel
|
||||
from module_admin.service.log_service import LogQueueService
|
||||
from utils.dependency_util import DependencyUtil
|
||||
from utils.log_util import logger
|
||||
from utils.response_util import ResponseUtil
|
||||
|
||||
P = ParamSpec('P')
|
||||
R = TypeVar('R')
|
||||
|
||||
|
||||
class Log:
|
||||
"""
|
||||
日志装饰器
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
business_type: BusinessType,
|
||||
log_type: Literal['login', 'operation'] | None = 'operation',
|
||||
) -> None:
|
||||
"""
|
||||
日志装饰器
|
||||
|
||||
:param title: 当前日志装饰器装饰的模块标题
|
||||
:param business_type: 业务类型(OTHER其它 INSERT新增 UPDATE修改 DELETE删除 GRANT授权 EXPORT导出 IMPORT导入 FORCE强退 GENCODE生成代码 CLEAN清空数据)
|
||||
:param log_type: 日志类型(login表示登录日志,operation表示为操作日志)
|
||||
:return:
|
||||
"""
|
||||
self.title = title
|
||||
self.business_type = business_type.value
|
||||
self.log_type = log_type
|
||||
self._oper_param_len = 2000
|
||||
|
||||
def __call__(self, func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
||||
@wraps(func)
|
||||
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
start_time = time.perf_counter()
|
||||
# 获取当前被装饰函数所在路径
|
||||
func_path = self._get_decorator_func_path(func)
|
||||
# 获取上下文信息
|
||||
request_name_list = get_function_parameters_name_by_type(func, Request)
|
||||
request = get_function_parameters_value_by_name(func, request_name_list[0], *args, **kwargs)
|
||||
DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用Log装饰器')
|
||||
request_method = request.method
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
# 获取操作类型
|
||||
operator_type = self._get_oper_type(user_agent)
|
||||
# 获取请求的url
|
||||
oper_url = request.url.path
|
||||
# 获取请求ip
|
||||
oper_ip = request.headers.get('X-Forwarded-For')
|
||||
# 获取请求ip归属区域
|
||||
oper_location = await self._get_oper_location(oper_ip)
|
||||
# 获取请求参数
|
||||
oper_param = await self._get_request_params(request)
|
||||
# 日志表请求参数字段长度最大为2000,因此在此处判断长度
|
||||
if len(oper_param) > self._oper_param_len:
|
||||
oper_param = '请求参数过长'
|
||||
|
||||
# 获取操作时间
|
||||
oper_time = datetime.now()
|
||||
# 此处在登录之前向原始函数传递一些登录信息,用于监测在线用户的相关信息
|
||||
login_log = self._get_login_log(user_agent, oper_ip, oper_location, oper_time, kwargs)
|
||||
try:
|
||||
# 调用原始函数
|
||||
result = await func(*args, **kwargs)
|
||||
except (LoginException, ServiceWarning) as e:
|
||||
logger.warning(e.message)
|
||||
result = ResponseUtil.failure(data=e.data, msg=e.message)
|
||||
except ServiceException as e:
|
||||
logger.error(e.message)
|
||||
result = ResponseUtil.error(data=e.data, msg=e.message)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
result = ResponseUtil.error(msg=str(e))
|
||||
# 获取请求耗时
|
||||
cost_time = float(time.perf_counter() - start_time) * 1000
|
||||
# 判断请求是否来自api文档
|
||||
request_from_swagger, request_from_redoc = self._is_request_from_swagger_or_redoc(request)
|
||||
# 根据响应结果的类型使用不同的方法获取响应结果参数
|
||||
result_dict = self._get_result_dict(result, request_from_swagger, request_from_redoc)
|
||||
json_result = json.dumps(result_dict, ensure_ascii=False)
|
||||
# 根据响应结果获取响应状态及异常信息
|
||||
status, error_msg = self._get_status_and_error_msg(result_dict)
|
||||
# 根据日志类型向对应的日志表插入数据
|
||||
if self.log_type == 'login':
|
||||
# 登录请求来自于api文档时不记录登录日志,其余情况则记录
|
||||
if request_from_swagger or request_from_redoc:
|
||||
pass
|
||||
else:
|
||||
user = kwargs.get('form_data')
|
||||
login_log.update(
|
||||
{
|
||||
'loginTime': oper_time,
|
||||
'userName': user.username,
|
||||
'status': str(status),
|
||||
'msg': result_dict.get('msg'),
|
||||
}
|
||||
)
|
||||
|
||||
await LogQueueService.enqueue_login_log(request, LogininforModel(**login_log), func_path)
|
||||
else:
|
||||
current_user = RequestContext.get_current_user()
|
||||
oper_name = current_user.user.user_name
|
||||
dept_name = current_user.user.dept.dept_name if current_user.user.dept else None
|
||||
operation_log = OperLogModel(
|
||||
title=self.title,
|
||||
businessType=self.business_type,
|
||||
method=func_path,
|
||||
requestMethod=request_method,
|
||||
operatorType=operator_type,
|
||||
operName=oper_name,
|
||||
deptName=dept_name,
|
||||
operUrl=oper_url,
|
||||
operIp=oper_ip,
|
||||
operLocation=oper_location,
|
||||
operParam=oper_param,
|
||||
jsonResult=json_result,
|
||||
status=status,
|
||||
errorMsg=error_msg,
|
||||
operTime=oper_time,
|
||||
costTime=int(cost_time),
|
||||
)
|
||||
await LogQueueService.enqueue_operation_log(request, operation_log, func_path)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
def _get_decorator_func_path(self, func: Callable) -> str:
|
||||
"""
|
||||
获取被装饰函数所在路径
|
||||
|
||||
:param func: 被装饰函数
|
||||
:return: 被装饰函数所在路径
|
||||
"""
|
||||
# 获取被装饰函数所在的模块
|
||||
module = inspect.getmodule(func)
|
||||
# 获取完整模块路径
|
||||
module_path = module.__name__ if module else ''
|
||||
# 获取当前被装饰函数所在路径
|
||||
func_path = f'{module_path}.{func.__name__}()'
|
||||
|
||||
return func_path
|
||||
|
||||
def _get_oper_type(self, user_agent: Any) -> int:
|
||||
"""
|
||||
获取操作类型
|
||||
|
||||
:param user_agent: 用户代理字符串
|
||||
:return: 操作类型
|
||||
"""
|
||||
operator_type = 0
|
||||
if 'Windows' in user_agent or 'Macintosh' in user_agent or 'Linux' in user_agent:
|
||||
operator_type = 1
|
||||
if 'Mobile' in user_agent or 'Android' in user_agent or 'iPhone' in user_agent:
|
||||
operator_type = 2
|
||||
|
||||
return operator_type
|
||||
|
||||
async def _get_oper_location(self, oper_ip: str) -> str:
|
||||
"""
|
||||
获取请求IP归属区域
|
||||
|
||||
:param oper_ip: 请求IP
|
||||
:return: 请求IP归属区域
|
||||
"""
|
||||
oper_location = '内网IP'
|
||||
if AppConfig.app_ip_location_query:
|
||||
oper_location = await get_ip_location(oper_ip)
|
||||
|
||||
return oper_location
|
||||
|
||||
async def _get_request_params(self, request: Request) -> str:
|
||||
"""
|
||||
获取请求参数
|
||||
|
||||
:param request: Request对象
|
||||
:return: 格式化后的请求参数字符串
|
||||
"""
|
||||
params = {}
|
||||
|
||||
# 路径和查询参数
|
||||
path_params = dict(request.path_params)
|
||||
query_params = dict(request.query_params)
|
||||
params.update({k: v for k, v in {'path_params': path_params, 'query_params': query_params}.items() if v})
|
||||
|
||||
# 请求体处理
|
||||
content_type = request.headers.get('Content-Type', '')
|
||||
|
||||
# JSON请求
|
||||
if 'application/json' in content_type:
|
||||
json_body = await request.json()
|
||||
if json_body:
|
||||
params['json_body'] = json_body
|
||||
|
||||
# 表单数据
|
||||
elif 'multipart/form-data' in content_type or 'application/x-www-form-urlencoded' in content_type:
|
||||
form_data = await request.form()
|
||||
if form_data:
|
||||
# 过滤掉文件对象,只保留普通表单字段
|
||||
form_dict = {key: value for key, value in form_data.items() if not hasattr(value, 'filename')}
|
||||
if form_dict:
|
||||
params['form_data'] = form_dict
|
||||
|
||||
# 仅在multipart时尝试处理文件
|
||||
if 'multipart/form-data' in content_type:
|
||||
file_info = {}
|
||||
for key, value in form_data.items():
|
||||
if hasattr(value, 'filename'):
|
||||
file_info[key] = {
|
||||
'filename': value.filename,
|
||||
'content_type': value.content_type,
|
||||
'size': value.size,
|
||||
'headers': dict(value.headers),
|
||||
}
|
||||
if file_info:
|
||||
params['files'] = file_info
|
||||
|
||||
# 其他文本请求
|
||||
elif 'application/octet-stream' not in content_type:
|
||||
body = await request.body()
|
||||
if body:
|
||||
params['raw_body'] = body.decode('utf-8')
|
||||
|
||||
return json.dumps(params, ensure_ascii=False, indent=2) if params else ''
|
||||
|
||||
def _get_login_log(
|
||||
self, user_agent: Any, oper_ip: str, oper_location: str, oper_time: datetime, origin_kwargs: dict
|
||||
) -> dict:
|
||||
"""
|
||||
获取登录日志信息
|
||||
|
||||
:param user_agent: 用户代理字符串
|
||||
:param oper_ip: 操作ip
|
||||
:param oper_location: 操作区域
|
||||
:param oper_time: 操作时间
|
||||
:param origin_kwargs: 原始函数参数
|
||||
:return: 登录日志信息
|
||||
"""
|
||||
login_log = {}
|
||||
if self.log_type == 'login':
|
||||
user_agent_info = parse(user_agent)
|
||||
browser = f'{user_agent_info.browser.family}'
|
||||
system_os = f'{user_agent_info.os.family}'
|
||||
if user_agent_info.browser.version != ():
|
||||
browser += f' {user_agent_info.browser.version[0]}'
|
||||
if user_agent_info.os.version != ():
|
||||
system_os += f' {user_agent_info.os.version[0]}'
|
||||
login_log = {
|
||||
'ipaddr': oper_ip,
|
||||
'loginLocation': oper_location,
|
||||
'browser': browser,
|
||||
'os': system_os,
|
||||
'loginTime': oper_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
}
|
||||
self._set_login_data(login_log, origin_kwargs)
|
||||
|
||||
return login_log
|
||||
|
||||
def _set_login_data(self, login_log: dict, origin_kwargs: dict) -> None:
|
||||
"""
|
||||
设置登录日志数据
|
||||
|
||||
:param login_log: 登录日志信息
|
||||
:param origin_kwargs: 原始函数参数
|
||||
:return: None
|
||||
"""
|
||||
if 'form_data' in origin_kwargs:
|
||||
origin_kwargs['form_data'].login_info = login_log
|
||||
|
||||
def _get_status_and_error_msg(self, result_dict: dict) -> tuple[int, str]:
|
||||
"""
|
||||
获取操作状态和错误信息
|
||||
|
||||
:param result_dict: 操作结果字典
|
||||
:return: 操作状态和错误信息元组
|
||||
"""
|
||||
status = 1
|
||||
error_msg = ''
|
||||
if result_dict.get('code') == HTTP_200_OK:
|
||||
status = 0
|
||||
else:
|
||||
error_msg = result_dict.get('msg')
|
||||
|
||||
return status, error_msg
|
||||
|
||||
def _is_request_from_swagger_or_redoc(self, request: Request) -> tuple[bool, bool]:
|
||||
"""
|
||||
判断请求是否来自swagger或redoc
|
||||
|
||||
:param request: Request对象
|
||||
:return: 是否来自swagger请求和是否来自redoc请求元组
|
||||
"""
|
||||
request_from_swagger = (
|
||||
request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False
|
||||
)
|
||||
request_from_redoc = (
|
||||
request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False
|
||||
)
|
||||
|
||||
return request_from_swagger, request_from_redoc
|
||||
|
||||
def _get_result_dict(self, result: Any, request_from_swagger: bool, request_from_redoc: bool) -> dict:
|
||||
"""
|
||||
获取操作结果字典
|
||||
|
||||
:param result: 操作结果
|
||||
:param request_from_swagger: 是否来自swagger请求
|
||||
:param request_from_redoc: 是否来自redoc请求
|
||||
:return: 操作结果字典
|
||||
"""
|
||||
if isinstance(result, (JSONResponse, ORJSONResponse, UJSONResponse)):
|
||||
result_dict = json.loads(str(result.body, 'utf-8'))
|
||||
elif request_from_swagger or request_from_redoc:
|
||||
result_dict = {}
|
||||
elif result.status_code == HTTP_200_OK:
|
||||
result_dict = {'code': result.status_code, 'message': '获取成功'}
|
||||
else:
|
||||
result_dict = {'code': result.status_code, 'message': '获取失败'}
|
||||
|
||||
return result_dict
|
||||
|
||||
|
||||
@alru_cache()
|
||||
async def get_ip_location(oper_ip: str) -> str:
|
||||
"""
|
||||
查询ip归属区域
|
||||
|
||||
:param oper_ip: 需要查询的ip
|
||||
:return: ip归属区域
|
||||
"""
|
||||
oper_location = '内网IP'
|
||||
try:
|
||||
if oper_ip not in ['127.0.0.1', 'localhost']:
|
||||
oper_location = '未知'
|
||||
async with httpx.AsyncClient() as client:
|
||||
ip_result = await client.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}')
|
||||
if ip_result.status_code == HTTP_200_OK:
|
||||
prov = ip_result.json().get('data', {}).get('prov')
|
||||
city = ip_result.json().get('data', {}).get('city')
|
||||
if prov or city:
|
||||
oper_location = f'{prov}-{city}'
|
||||
except Exception as e:
|
||||
oper_location = '未知'
|
||||
print(e)
|
||||
return oper_location
|
||||
|
||||
|
||||
def get_function_parameters_name_by_type(func: Callable, param_type: Any) -> list:
|
||||
"""
|
||||
获取函数指定类型的参数名称
|
||||
|
||||
:param func: 函数
|
||||
:param arg_type: 参数类型
|
||||
:return: 函数指定类型的参数名称
|
||||
"""
|
||||
# 获取函数的参数信息
|
||||
parameters = inspect.signature(func).parameters
|
||||
# 找到指定类型的参数名称
|
||||
parameters_name_list = []
|
||||
# 遍历所有参数
|
||||
for name, param in parameters.items():
|
||||
# 处理参数注解
|
||||
annotation = param.annotation
|
||||
# 检查参数类型是否匹配
|
||||
# 1. 直接匹配
|
||||
# 2. 检查是否为Annotated类型(通过类型名称判断)
|
||||
if annotation == param_type or (
|
||||
hasattr(annotation, '__class__')
|
||||
and annotation.__class__.__name__ == '_AnnotatedAlias'
|
||||
and annotation.__origin__ == param_type
|
||||
):
|
||||
parameters_name_list.append(name)
|
||||
return parameters_name_list
|
||||
|
||||
|
||||
def get_function_parameters_value_by_name(func: Callable, name: str, *args, **kwargs) -> Any:
|
||||
"""
|
||||
获取函数指定参数的值
|
||||
|
||||
:param func: 函数
|
||||
:param name: 参数名
|
||||
:return: 参数值
|
||||
"""
|
||||
# 获取参数值
|
||||
bound_parameters = inspect.signature(func).bind(*args, **kwargs)
|
||||
bound_parameters.apply_defaults()
|
||||
parameters_value = bound_parameters.arguments.get(name)
|
||||
|
||||
return parameters_value
|
||||
@@ -1,27 +1,31 @@
|
||||
import inspect
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from fastapi import Query, Form
|
||||
from fastapi import Form, Query
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
BaseModelVar = TypeVar('BaseModelVar', bound=BaseModel)
|
||||
|
||||
|
||||
def as_query(cls: Type[BaseModel]):
|
||||
def as_query(cls: type[BaseModelVar]) -> type[BaseModelVar]:
|
||||
"""
|
||||
pydantic模型查询参数装饰器,将pydantic模型用于接收查询参数
|
||||
"""
|
||||
new_parameters = []
|
||||
|
||||
for field_name, model_field in cls.model_fields.items():
|
||||
model_field: FieldInfo # type: ignore
|
||||
for model_field in cls.model_fields.values():
|
||||
model_field: FieldInfo
|
||||
|
||||
if not model_field.is_required():
|
||||
new_parameters.append(
|
||||
inspect.Parameter(
|
||||
model_field.alias,
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
default=Query(model_field.default),
|
||||
annotation=model_field.annotation
|
||||
default=Query(default=model_field.default, description=model_field.description),
|
||||
annotation=model_field.annotation,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -29,37 +33,37 @@ def as_query(cls: Type[BaseModel]):
|
||||
inspect.Parameter(
|
||||
model_field.alias,
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
default=Query(...),
|
||||
annotation=model_field.annotation
|
||||
default=Query(..., description=model_field.description),
|
||||
annotation=model_field.annotation,
|
||||
)
|
||||
)
|
||||
|
||||
async def as_query_func(**data):
|
||||
async def as_query_func(**data) -> type[BaseModelVar]:
|
||||
return cls(**data)
|
||||
|
||||
sig = inspect.signature(as_query_func)
|
||||
sig = sig.replace(parameters=new_parameters)
|
||||
as_query_func.__signature__ = sig # type: ignore
|
||||
setattr(cls, 'as_query', as_query_func)
|
||||
as_query_func.__signature__ = sig
|
||||
cls.as_query = as_query_func
|
||||
return cls
|
||||
|
||||
|
||||
def as_form(cls: Type[BaseModel]):
|
||||
def as_form(cls: type[BaseModelVar]) -> type[BaseModelVar]:
|
||||
"""
|
||||
pydantic模型表单参数装饰器,将pydantic模型用于接收表单参数
|
||||
"""
|
||||
new_parameters = []
|
||||
|
||||
for field_name, model_field in cls.model_fields.items():
|
||||
model_field: FieldInfo # type: ignore
|
||||
for model_field in cls.model_fields.values():
|
||||
model_field: FieldInfo
|
||||
|
||||
if not model_field.is_required():
|
||||
new_parameters.append(
|
||||
inspect.Parameter(
|
||||
model_field.alias,
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
default=Form(model_field.default),
|
||||
annotation=model_field.annotation
|
||||
default=Form(default=model_field.default, description=model_field.description),
|
||||
annotation=model_field.annotation,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -67,16 +71,16 @@ def as_form(cls: Type[BaseModel]):
|
||||
inspect.Parameter(
|
||||
model_field.alias,
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
default=Form(...),
|
||||
annotation=model_field.annotation
|
||||
default=Form(..., description=model_field.description),
|
||||
annotation=model_field.annotation,
|
||||
)
|
||||
)
|
||||
|
||||
async def as_form_func(**data):
|
||||
async def as_form_func(**data) -> type[BaseModelVar]:
|
||||
return cls(**data)
|
||||
|
||||
sig = inspect.signature(as_form_func)
|
||||
sig = sig.replace(parameters=new_parameters)
|
||||
as_form_func.__signature__ = sig # type: ignore
|
||||
setattr(cls, 'as_form', as_form_func)
|
||||
as_form_func.__signature__ = sig
|
||||
cls.as_form = as_form_func
|
||||
return cls
|
||||
@@ -0,0 +1,112 @@
|
||||
from fastapi import Depends, Request, params
|
||||
from sqlalchemy import ColumnElement, func, or_, select
|
||||
|
||||
from common.context import RequestContext
|
||||
from config.database import Base
|
||||
from module_admin.entity.do.dept_do import SysDept
|
||||
from module_admin.entity.do.role_do import SysRoleDept
|
||||
from utils.dependency_util import DependencyUtil
|
||||
|
||||
|
||||
class GetDataScope:
|
||||
"""
|
||||
获取当前用户数据权限对应的查询sql语句
|
||||
"""
|
||||
|
||||
DATA_SCOPE_ALL = '1'
|
||||
DATA_SCOPE_CUSTOM = '2'
|
||||
DATA_SCOPE_DEPT = '3'
|
||||
DATA_SCOPE_DEPT_AND_CHILD = '4'
|
||||
DATA_SCOPE_SELF = '5'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_alias: Base,
|
||||
user_alias: str | None = 'user_id',
|
||||
dept_alias: str | None = 'dept_id',
|
||||
) -> None:
|
||||
"""
|
||||
获取当前用户数据权限对应的查询sql语句
|
||||
|
||||
:param query_alias: 所要查询表对应的sqlalchemy模型类,不可为空
|
||||
:param user_alias: 用户id字段别名,默认为'user_id'
|
||||
:param dept_alias: 部门id字段别名,默认为'dept_id'
|
||||
"""
|
||||
self.query_alias = query_alias
|
||||
self.user_alias = user_alias
|
||||
self.dept_alias = dept_alias
|
||||
|
||||
def __call__(self, request: Request) -> ColumnElement:
|
||||
DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用GetDataScope依赖项')
|
||||
current_user = RequestContext.get_current_user()
|
||||
user_id = current_user.user.user_id
|
||||
dept_id = current_user.user.dept_id
|
||||
custom_data_scope_role_id_list = [
|
||||
item.role_id for item in current_user.user.role if item.data_scope == self.DATA_SCOPE_CUSTOM
|
||||
]
|
||||
param_sql_list = []
|
||||
for role in current_user.user.role:
|
||||
if current_user.user.admin or role.data_scope == self.DATA_SCOPE_ALL:
|
||||
param_sql_list = [True]
|
||||
break
|
||||
if role.data_scope == self.DATA_SCOPE_CUSTOM:
|
||||
if len(custom_data_scope_role_id_list) > 1:
|
||||
param_sql_list.append(
|
||||
getattr(self.query_alias, self.dept_alias).in_(
|
||||
select(SysRoleDept.dept_id).where(SysRoleDept.role_id.in_(custom_data_scope_role_id_list))
|
||||
)
|
||||
if hasattr(self.query_alias, self.dept_alias)
|
||||
else False
|
||||
)
|
||||
else:
|
||||
param_sql_list.append(
|
||||
getattr(self.query_alias, self.dept_alias).in_(
|
||||
select(SysRoleDept.dept_id).where(SysRoleDept.role_id == role.role_id)
|
||||
)
|
||||
if hasattr(self.query_alias, self.dept_alias)
|
||||
else False
|
||||
)
|
||||
elif role.data_scope == self.DATA_SCOPE_DEPT:
|
||||
param_sql_list.append(
|
||||
getattr(self.query_alias, self.dept_alias) == dept_id
|
||||
if hasattr(self.query_alias, self.dept_alias)
|
||||
else False
|
||||
)
|
||||
elif role.data_scope == self.DATA_SCOPE_DEPT_AND_CHILD:
|
||||
param_sql_list.append(
|
||||
getattr(self.query_alias, self.dept_alias).in_(
|
||||
select(SysDept.dept_id).where(
|
||||
or_(SysDept.dept_id == dept_id, func.find_in_set(dept_id, SysDept.ancestors))
|
||||
)
|
||||
)
|
||||
if hasattr(self.query_alias, self.dept_alias)
|
||||
else False
|
||||
)
|
||||
elif role.data_scope == self.DATA_SCOPE_SELF:
|
||||
param_sql_list.append(
|
||||
getattr(self.query_alias, self.user_alias) == user_id
|
||||
if hasattr(self.query_alias, self.user_alias)
|
||||
else False
|
||||
)
|
||||
else:
|
||||
param_sql_list.append(False)
|
||||
param_sql_list = list(dict.fromkeys(param_sql_list))
|
||||
param_sql = or_(*param_sql_list)
|
||||
|
||||
return param_sql
|
||||
|
||||
|
||||
def DataScopeDependency( # noqa: N802
|
||||
query_alias: Base,
|
||||
user_alias: str | None = 'user_id',
|
||||
dept_alias: str | None = 'dept_id',
|
||||
) -> params.Depends:
|
||||
"""
|
||||
当前用户数据权限依赖
|
||||
|
||||
:param query_alias: 所要查询表对应的sqlalchemy模型类,不可为空
|
||||
:param user_alias: 用户id字段别名,默认为'user_id'
|
||||
:param dept_alias: 部门id字段别名,默认为'dept_id'
|
||||
:return: 当前用户数据权限依赖
|
||||
"""
|
||||
return Depends(GetDataScope(query_alias, user_alias, dept_alias))
|
||||
@@ -0,0 +1,12 @@
|
||||
from fastapi import Depends, params
|
||||
|
||||
from config.get_db import get_db
|
||||
|
||||
|
||||
def DBSessionDependency() -> params.Depends: # noqa: N802
|
||||
"""
|
||||
数据库会话依赖
|
||||
|
||||
:return: 数据库会话依赖
|
||||
"""
|
||||
return Depends(get_db)
|
||||
@@ -0,0 +1,94 @@
|
||||
from fastapi import Depends, Request, params
|
||||
|
||||
from common.context import RequestContext
|
||||
from exceptions.exception import PermissionException
|
||||
from utils.dependency_util import DependencyUtil
|
||||
|
||||
|
||||
class CheckUserInterfaceAuth:
|
||||
"""
|
||||
校验当前用户是否具有相应的接口权限
|
||||
"""
|
||||
|
||||
def __init__(self, perm: str | list, is_strict: bool = False) -> None:
|
||||
"""
|
||||
校验当前用户是否具有相应的接口权限
|
||||
|
||||
:param perm: 权限标识
|
||||
:param is_strict: 当传入的权限标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个权限标识,所有的校验结果都需要为True才会通过
|
||||
"""
|
||||
self.perm = perm
|
||||
self.is_strict = is_strict
|
||||
|
||||
def __call__(self, request: Request) -> bool:
|
||||
DependencyUtil.check_exclude_routes(
|
||||
request, err_msg='当前路由不在认证规则内,不可使用CheckUserInterfaceAuth依赖项'
|
||||
)
|
||||
current_user = RequestContext.get_current_user()
|
||||
user_auth_list = current_user.permissions
|
||||
if '*:*:*' in user_auth_list:
|
||||
return True
|
||||
if isinstance(self.perm, str) and self.perm in user_auth_list:
|
||||
return True
|
||||
if isinstance(self.perm, list):
|
||||
if self.is_strict:
|
||||
if all(perm_str in user_auth_list for perm_str in self.perm):
|
||||
return True
|
||||
elif any(perm_str in user_auth_list for perm_str in self.perm):
|
||||
return True
|
||||
raise PermissionException(data='', message='该用户无此接口权限')
|
||||
|
||||
|
||||
class CheckRoleInterfaceAuth:
|
||||
"""
|
||||
根据角色校验当前用户是否具有相应的接口权限
|
||||
"""
|
||||
|
||||
def __init__(self, role_key: str | list, is_strict: bool = False) -> None:
|
||||
"""
|
||||
根据角色校验当前用户是否具有相应的接口权限
|
||||
|
||||
:param role_key: 角色标识
|
||||
:param is_strict: 当传入的角色标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个角色标识,所有的校验结果都需要为True才会通过
|
||||
"""
|
||||
self.role_key = role_key
|
||||
self.is_strict = is_strict
|
||||
|
||||
def __call__(self, request: Request) -> bool:
|
||||
DependencyUtil.check_exclude_routes(
|
||||
request, err_msg='当前路由不在认证规则内,不可使用CheckRoleInterfaceAuth依赖项'
|
||||
)
|
||||
current_user = RequestContext.get_current_user()
|
||||
user_role_list = current_user.user.role
|
||||
user_role_key_list = [role.role_key for role in user_role_list]
|
||||
if isinstance(self.role_key, str) and self.role_key in user_role_key_list:
|
||||
return True
|
||||
if isinstance(self.role_key, list):
|
||||
if self.is_strict:
|
||||
if all(role_key_str in user_role_key_list for role_key_str in self.role_key):
|
||||
return True
|
||||
elif any(role_key_str in user_role_key_list for role_key_str in self.role_key):
|
||||
return True
|
||||
raise PermissionException(data='', message='该用户无此接口权限')
|
||||
|
||||
|
||||
def UserInterfaceAuthDependency(perm: str | list, is_strict: bool = False) -> params.Depends: # noqa: N802
|
||||
"""
|
||||
根据权限标识校验当前用户接口权限依赖
|
||||
|
||||
:param perm: 权限标识
|
||||
:param is_strict: 当传入的权限标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个权限标识,所有的校验结果都需要为True才会通过
|
||||
:return: 根据权限标识校验当前用户接口权限依赖
|
||||
"""
|
||||
return Depends(CheckUserInterfaceAuth(perm, is_strict))
|
||||
|
||||
|
||||
def RoleInterfaceAuthDependency(role_key: str | list, is_strict: bool = False) -> params.Depends: # noqa: N802
|
||||
"""
|
||||
根据角色校验当前用户接口权限依赖
|
||||
|
||||
:param role_key: 角色标识
|
||||
:param is_strict: 当传入的角色标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个角色标识,所有的校验结果都需要为True才会通过
|
||||
:return: 根据角色校验当前用户接口权限依赖
|
||||
"""
|
||||
return Depends(CheckRoleInterfaceAuth(role_key, is_strict))
|
||||
@@ -0,0 +1,146 @@
|
||||
import re
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from fastapi import Depends, Request, params
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.context import RequestContext
|
||||
from config.env import AppConfig
|
||||
from config.get_db import get_db
|
||||
from exceptions.exception import AuthException
|
||||
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||
from module_admin.service.login_service import LoginService
|
||||
|
||||
|
||||
# 定义排除路由的字典结构
|
||||
class ExcludeRoute(TypedDict, total=False):
|
||||
"""
|
||||
排除路由的字典结构
|
||||
|
||||
:param path: 路由路径(必填)
|
||||
:param methods: HTTP方法列表,空列表表示所有方法(可选,默认为[])
|
||||
:param ignore_paths: 需要忽略的特定路径列表,即使匹配通配符也不排除(可选,默认为[])
|
||||
"""
|
||||
|
||||
path: str
|
||||
methods: list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']]
|
||||
ignore_paths: list[str]
|
||||
|
||||
|
||||
# 创建OAuth2PasswordBearer对象
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/login')
|
||||
|
||||
|
||||
class PreAuth:
|
||||
"""
|
||||
登录认证前置校验依赖类
|
||||
"""
|
||||
|
||||
def __init__(self, exclude_routes: list[ExcludeRoute] | None = None) -> None:
|
||||
"""
|
||||
初始化登录认证前置校验依赖
|
||||
|
||||
:param exclude_routes: 需要排除的路由列表,格式为:
|
||||
[{'path': '/path1', 'methods': ['GET', 'POST']}, {'path': '/path2/{param}', 'methods': ['GET']}]
|
||||
methods 可以是字符串或列表,空列表表示所有方法
|
||||
"""
|
||||
self.exclude_routes = exclude_routes or []
|
||||
# 编译排除路径为正则表达式模式,并存储方法信息
|
||||
self.exclude_patterns = []
|
||||
|
||||
for route in self.exclude_routes:
|
||||
# 使用TypedDict,确保路由字典包含path字段
|
||||
path = route.get('path', '')
|
||||
methods = route.get('methods', [])
|
||||
ignore_paths = route.get('ignore_paths', [])
|
||||
|
||||
# 编译路径为正则表达式
|
||||
pattern = self._compile_path_pattern(path)
|
||||
# 存储编译后的模式和方法信息
|
||||
self.exclude_patterns.append(
|
||||
{
|
||||
'pattern': pattern,
|
||||
'methods': [method.upper() for method in methods],
|
||||
'original_path': path,
|
||||
'ignore_paths': ignore_paths,
|
||||
}
|
||||
)
|
||||
|
||||
def _compile_path_pattern(self, path: str) -> re.Pattern:
|
||||
"""
|
||||
将FastAPI路径转换为正则表达式模式
|
||||
|
||||
:param path: FastAPI路径(如 /configKey/{config_key})
|
||||
:return: 编译后的正则表达式模式
|
||||
"""
|
||||
# 将FastAPI路径参数转换为正则表达式
|
||||
# 例如:/configKey/{config_key} -> /configKey/[^/]+
|
||||
pattern_str = re.sub(r'\{[^}]+\}', r'[^/]+', path)
|
||||
# 添加开始和结束锚点,确保精确匹配
|
||||
return re.compile(f'^{pattern_str}$')
|
||||
|
||||
async def __call__(self, request: Request, db: AsyncSession = Depends(get_db)) -> CurrentUserModel | None:
|
||||
"""
|
||||
执行登录认证校验
|
||||
|
||||
:param request: 当前请求对象
|
||||
:param db: 数据库会话
|
||||
:return: 当前用户信息
|
||||
"""
|
||||
# 获取当前请求路径和方法
|
||||
path = request.url.path
|
||||
method = request.method.upper()
|
||||
|
||||
# 从配置中获取APP_ROOT_PATH
|
||||
app_root_path = AppConfig.app_root_path
|
||||
|
||||
# 去掉APP_ROOT_PATH前缀
|
||||
if app_root_path and path.startswith(app_root_path):
|
||||
path = path[len(app_root_path) :]
|
||||
|
||||
# 设置上下文变量
|
||||
RequestContext.set_current_exclude_patterns(self.exclude_patterns)
|
||||
|
||||
# 检查路径和方法是否匹配排除模式
|
||||
for item in self.exclude_patterns:
|
||||
pattern = item['pattern']
|
||||
exclude_methods = item['methods']
|
||||
ignore_paths = item['ignore_paths']
|
||||
|
||||
# 检查当前路径是否在忽略列表中
|
||||
if path in ignore_paths:
|
||||
continue
|
||||
|
||||
# 检查路径是否匹配,并且methods为空列表(匹配所有方法)或者当前方法在允许列表中
|
||||
if pattern.match(path) and (not exclude_methods or method in exclude_methods):
|
||||
# 跳过认证
|
||||
return None
|
||||
|
||||
# 否则执行正常认证
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
raise AuthException(data='', message='用户未登录,请先完成登录')
|
||||
current_user = await LoginService.get_current_user(request, token, db)
|
||||
return current_user
|
||||
|
||||
|
||||
def PreAuthDependency(exclude_routes: list[ExcludeRoute] | None = None) -> params.Depends: # noqa: N802
|
||||
"""
|
||||
登录认证前置校验依赖
|
||||
|
||||
:param exclude_routes: 需要排除的路由列表,格式为:
|
||||
[{'path': '/path1', 'methods': ['GET', 'POST']}, {'path': '/path2/{param}', 'methods': ['GET']}]
|
||||
methods 可以是字符串或列表,空列表表示所有方法
|
||||
:return: 登录认证前置校验依赖
|
||||
"""
|
||||
return Depends(PreAuth(exclude_routes))
|
||||
|
||||
|
||||
def CurrentUserDependency() -> params.Depends: # noqa: N802
|
||||
"""
|
||||
当前登录用户信息依赖
|
||||
|
||||
:return: 当前登录用户信息依赖
|
||||
"""
|
||||
return Depends(LoginService.get_current_user)
|
||||
@@ -0,0 +1,495 @@
|
||||
from config.env import DataBaseConfig
|
||||
|
||||
|
||||
class CommonConstant:
|
||||
"""
|
||||
常用常量
|
||||
|
||||
PASSWORD_ERROR_COUNT: 密码错误次数
|
||||
WWW: www主域
|
||||
HTTP: http请求
|
||||
HTTPS: https请求
|
||||
LOOKUP_RMI: RMI远程方法调用
|
||||
LOOKUP_LDAP: LDAP远程方法调用
|
||||
LOOKUP_LDAPS: LDAPS远程方法调用
|
||||
YES: 是否为系统默认(是)
|
||||
NO: 是否为系统默认(否)
|
||||
DEPT_NORMAL: 部门正常状态
|
||||
DEPT_DISABLE: 部门停用状态
|
||||
UNIQUE: 校验是否唯一的返回标识(是)
|
||||
NOT_UNIQUE: 校验是否唯一的返回标识(否)
|
||||
"""
|
||||
|
||||
PASSWORD_ERROR_COUNT = 5
|
||||
WWW = 'www.'
|
||||
HTTP = 'http://'
|
||||
HTTPS = 'https://'
|
||||
LOOKUP_RMI = 'rmi:'
|
||||
LOOKUP_LDAP = 'ldap:'
|
||||
LOOKUP_LDAPS = 'ldaps:'
|
||||
YES = 'Y'
|
||||
NO = 'N'
|
||||
DEPT_NORMAL = '0'
|
||||
DEPT_DISABLE = '1'
|
||||
UNIQUE = True
|
||||
NOT_UNIQUE = False
|
||||
|
||||
|
||||
class HttpStatusConstant:
|
||||
"""
|
||||
返回状态码
|
||||
|
||||
SUCCESS: 操作成功
|
||||
CREATED: 对象创建成功
|
||||
ACCEPTED: 请求已经被接受
|
||||
NO_CONTENT: 操作已经执行成功,但是没有返回数据
|
||||
MOVED_PERM: 资源已被移除
|
||||
SEE_OTHER: 重定向
|
||||
NOT_MODIFIED: 资源没有被修改
|
||||
BAD_REQUEST: 参数列表错误(缺少,格式不匹配)
|
||||
UNAUTHORIZED: 未授权
|
||||
FORBIDDEN: 访问受限,授权过期
|
||||
NOT_FOUND: 资源,服务未找到
|
||||
BAD_METHOD: 不允许的http方法
|
||||
CONFLICT: 资源冲突,或者资源被锁
|
||||
UNSUPPORTED_TYPE: 不支持的数据,媒体类型
|
||||
ERROR: 系统内部错误
|
||||
NOT_IMPLEMENTED: 接口未实现
|
||||
WARN: 系统警告消息
|
||||
"""
|
||||
|
||||
SUCCESS = 200
|
||||
CREATED = 201
|
||||
ACCEPTED = 202
|
||||
NO_CONTENT = 204
|
||||
MOVED_PERM = 301
|
||||
SEE_OTHER = 303
|
||||
NOT_MODIFIED = 304
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
BAD_METHOD = 405
|
||||
CONFLICT = 409
|
||||
UNSUPPORTED_TYPE = 415
|
||||
ERROR = 500
|
||||
NOT_IMPLEMENTED = 501
|
||||
WARN = 601
|
||||
|
||||
|
||||
class JobConstant:
|
||||
"""
|
||||
定时任务常量
|
||||
|
||||
JOB_ERROR_LIST: 定时任务禁止调用模块及违规字符串列表
|
||||
JOB_WHITE_LIST: 定时任务允许调用模块列表
|
||||
"""
|
||||
|
||||
JOB_ERROR_LIST = [
|
||||
'app',
|
||||
'config',
|
||||
'exceptions',
|
||||
'import ',
|
||||
'middlewares',
|
||||
'module_admin',
|
||||
'open(',
|
||||
'os.',
|
||||
'server',
|
||||
'sub_applications',
|
||||
'subprocess.',
|
||||
'sys.',
|
||||
'utils',
|
||||
'while ',
|
||||
'__import__',
|
||||
'"',
|
||||
"'",
|
||||
',',
|
||||
'?',
|
||||
':',
|
||||
';',
|
||||
'/',
|
||||
'|',
|
||||
'+',
|
||||
'-',
|
||||
'=',
|
||||
'~',
|
||||
'!',
|
||||
'#',
|
||||
'$',
|
||||
'%',
|
||||
'^',
|
||||
'&',
|
||||
'*',
|
||||
'<',
|
||||
'>',
|
||||
'(',
|
||||
')',
|
||||
'[',
|
||||
']',
|
||||
'{',
|
||||
'}',
|
||||
' ',
|
||||
]
|
||||
JOB_WHITE_LIST = ['module_task']
|
||||
|
||||
|
||||
class LockConstant:
|
||||
"""
|
||||
分布式锁常量
|
||||
"""
|
||||
|
||||
APP_STARTUP_LOCK_KEY = 'app:startup:lock'
|
||||
LOCK_EXPIRE_SECONDS = 60
|
||||
LOCK_RENEWAL_INTERVAL = 20
|
||||
|
||||
|
||||
class MenuConstant:
|
||||
"""
|
||||
菜单常量
|
||||
|
||||
TYPE_DIR: 菜单类型(目录)
|
||||
TYPE_MENU: 菜单类型(菜单)
|
||||
TYPE_BUTTON: 菜单类型(按钮)
|
||||
YES_FRAME: 是否菜单外链(是)
|
||||
NO_FRAME: 是否菜单外链(否)
|
||||
LAYOUT: Layout组件标识
|
||||
PARENT_VIEW: ParentView组件标识
|
||||
INNER_LINK: InnerLink组件标识
|
||||
"""
|
||||
|
||||
TYPE_DIR = 'M'
|
||||
TYPE_MENU = 'C'
|
||||
TYPE_BUTTON = 'F'
|
||||
YES_FRAME = 0
|
||||
NO_FRAME = 1
|
||||
LAYOUT = 'Layout'
|
||||
PARENT_VIEW = 'ParentView'
|
||||
INNER_LINK = 'InnerLink'
|
||||
|
||||
|
||||
class GenConstant:
|
||||
"""
|
||||
代码生成常量
|
||||
|
||||
TPL_CRUD: 单表(增删改查
|
||||
TPL_TREE: 树表(增删改查)
|
||||
TPL_SUB: 主子表(增删改查)
|
||||
TREE_CODE: 树编码字段
|
||||
TREE_PARENT_CODE: 树父编码字段
|
||||
TREE_NAME: 树名称字段
|
||||
PARENT_MENU_ID: 上级菜单ID字段
|
||||
PARENT_MENU_NAME: 上级菜单名称字段
|
||||
COLUMNTYPE_STR: 数据库字符串类型
|
||||
COLUMNTYPE_TEXT: 数据库文本类型
|
||||
COLUMNTYPE_TIME: 数据库时间类型
|
||||
COLUMNTYPE_GEOMETRY: 数据库字空间类型
|
||||
COLUMNTYPE_NUMBER: 数据库数字类型
|
||||
COLUMNNAME_NOT_EDIT: 页面不需要编辑字段
|
||||
COLUMNNAME_NOT_LIST: 页面不需要显示的列表字段
|
||||
COLUMNNAME_NOT_QUERY: 页面不需要查询字段
|
||||
BASE_ENTITY: Entity基类字段
|
||||
TREE_ENTITY: Tree基类字段
|
||||
HTML_INPUT: 文本框
|
||||
HTML_TEXTAREA: 文本域
|
||||
HTML_SELECT: 下拉框
|
||||
HTML_RADIO: 单选框
|
||||
HTML_CHECKBOX: 复选框
|
||||
HTML_DATETIME: 日期控件
|
||||
HTML_IMAGE_UPLOAD: 图片上传控件
|
||||
HTML_FILE_UPLOAD: 文件上传控件
|
||||
HTML_EDITOR: 富文本控件
|
||||
TYPE_DECIMAL: 高精度计算类型
|
||||
TYPE_DATE: 时间类型
|
||||
QUERY_LIKE: 模糊查询
|
||||
QUERY_EQ: 相等查询
|
||||
REQUIRE: 需要
|
||||
DB_TO_SQLALCHEMY_TYPE_MAPPING: 数据库类型与sqlalchemy类型映射
|
||||
DB_TO_PYTHON_TYPE_MAPPING: 数据库类型与python类型映射
|
||||
"""
|
||||
|
||||
TPL_CRUD = 'crud'
|
||||
TPL_TREE = 'tree'
|
||||
TPL_SUB = 'sub'
|
||||
TREE_CODE = 'treeCode'
|
||||
TREE_PARENT_CODE = 'treeParentCode'
|
||||
TREE_NAME = 'treeName'
|
||||
PARENT_MENU_ID = 'parentMenuId'
|
||||
PARENT_MENU_NAME = 'parentMenuName'
|
||||
COLUMNTYPE_STR = (
|
||||
['character varying', 'varchar', 'character', 'char']
|
||||
if DataBaseConfig.db_type == 'postgresql'
|
||||
else ['char', 'varchar', 'nvarchar', 'varchar2']
|
||||
)
|
||||
COLUMNTYPE_TEXT = (
|
||||
['text', 'citext'] if DataBaseConfig.db_type == 'postgresql' else ['tinytext', 'text', 'mediumtext', 'longtext']
|
||||
)
|
||||
COLUMNTYPE_TIME = (
|
||||
[
|
||||
'date',
|
||||
'time',
|
||||
'time with time zone',
|
||||
'time without time zone',
|
||||
'timestamp',
|
||||
'timestamp with time zone',
|
||||
'timestamp without time zone',
|
||||
'interval',
|
||||
]
|
||||
if DataBaseConfig.db_type == 'postgresql'
|
||||
else ['datetime', 'time', 'date', 'timestamp']
|
||||
)
|
||||
COLUMNTYPE_GEOMETRY = (
|
||||
['point', 'line', 'lseg', 'box', 'path', 'polygon', 'circle']
|
||||
if DataBaseConfig.db_type == 'postgresql'
|
||||
else [
|
||||
'geometry',
|
||||
'point',
|
||||
'linestring',
|
||||
'polygon',
|
||||
'multipoint',
|
||||
'multilinestring',
|
||||
'multipolygon',
|
||||
'geometrycollection',
|
||||
]
|
||||
)
|
||||
COLUMNTYPE_NUMBER = [
|
||||
'tinyint',
|
||||
'smallint',
|
||||
'mediumint',
|
||||
'int',
|
||||
'number',
|
||||
'integer',
|
||||
'bit',
|
||||
'bigint',
|
||||
'float',
|
||||
'double',
|
||||
'decimal',
|
||||
]
|
||||
COLUMNNAME_NOT_ADD_SHOW = ['create_by', 'create_time']
|
||||
COLUMNNAME_NOT_EDIT_SHOW = ['update_by', 'update_time']
|
||||
COLUMNNAME_NOT_EDIT = ['id', 'create_by', 'create_time', 'del_flag']
|
||||
COLUMNNAME_NOT_LIST = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time']
|
||||
COLUMNNAME_NOT_QUERY = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time', 'remark']
|
||||
BASE_ENTITY = ['createBy', 'createTime', 'updateBy', 'updateTime', 'remark']
|
||||
TREE_ENTITY = ['parentName', 'parentId', 'orderNum', 'ancestors', 'children']
|
||||
HTML_INPUT = 'input'
|
||||
HTML_TEXTAREA = 'textarea'
|
||||
HTML_SELECT = 'select'
|
||||
HTML_RADIO = 'radio'
|
||||
HTML_CHECKBOX = 'checkbox'
|
||||
HTML_DATETIME = 'datetime'
|
||||
HTML_IMAGE_UPLOAD = 'imageUpload'
|
||||
HTML_FILE_UPLOAD = 'fileUpload'
|
||||
HTML_EDITOR = 'editor'
|
||||
TYPE_DECIMAL = 'Decimal'
|
||||
TYPE_DATE = ['date', 'time', 'datetime']
|
||||
QUERY_LIKE = 'LIKE'
|
||||
QUERY_EQ = 'EQ'
|
||||
REQUIRE = '1'
|
||||
DB_TO_SQLALCHEMY_TYPE_MAPPING = (
|
||||
{
|
||||
'boolean': 'Boolean',
|
||||
'smallint': 'SmallInteger',
|
||||
'integer': 'Integer',
|
||||
'bigint': 'BigInteger',
|
||||
'real': 'Float',
|
||||
'double precision': 'Float',
|
||||
'numeric': 'Numeric',
|
||||
'character varying': 'String',
|
||||
'character': 'String',
|
||||
'text': 'Text',
|
||||
'bytea': 'LargeBinary',
|
||||
'date': 'Date',
|
||||
'time': 'Time',
|
||||
'time with time zone': 'Time',
|
||||
'time without time zone': 'Time',
|
||||
'timestamp': 'DateTime',
|
||||
'timestamp with time zone': 'DateTime',
|
||||
'timestamp without time zone': 'DateTime',
|
||||
'interval': 'Interval',
|
||||
'json': 'JSON',
|
||||
'jsonb': 'JSONB',
|
||||
'uuid': 'Uuid',
|
||||
'inet': 'INET',
|
||||
'cidr': 'CIDR',
|
||||
'macaddr': 'MACADDR',
|
||||
'point': 'Geometry',
|
||||
'line': 'Geometry',
|
||||
'lseg': 'Geometry',
|
||||
'box': 'Geometry',
|
||||
'path': 'Geometry',
|
||||
'polygon': 'Geometry',
|
||||
'circle': 'Geometry',
|
||||
'bit': 'Bit',
|
||||
'bit varying': 'Bit',
|
||||
'tsvector': 'TSVECTOR',
|
||||
'tsquery': 'TSQUERY',
|
||||
'xml': 'String',
|
||||
'array': 'ARRAY',
|
||||
'composite': 'JSON',
|
||||
'enum': 'Enum',
|
||||
'range': 'Range',
|
||||
'money': 'Numeric',
|
||||
'pg_lsn': 'BigInteger',
|
||||
'txid_snapshot': 'String',
|
||||
'oid': 'BigInteger',
|
||||
'regproc': 'String',
|
||||
'regclass': 'String',
|
||||
'regtype': 'String',
|
||||
'regrole': 'String',
|
||||
'regnamespace': 'String',
|
||||
'int2vector': 'ARRAY',
|
||||
'oidvector': 'ARRAY',
|
||||
'pg_node_tree': 'Text',
|
||||
}
|
||||
if DataBaseConfig.db_type == 'postgresql'
|
||||
else {
|
||||
# 数值类型
|
||||
'TINYINT': 'SmallInteger',
|
||||
'SMALLINT': 'SmallInteger',
|
||||
'MEDIUMINT': 'Integer',
|
||||
'INT': 'Integer',
|
||||
'INTEGER': 'Integer',
|
||||
'BIGINT': 'BigInteger',
|
||||
'FLOAT': 'Float',
|
||||
'DOUBLE': 'Float',
|
||||
'DECIMAL': 'DECIMAL',
|
||||
'BIT': 'Integer',
|
||||
# 日期和时间类型
|
||||
'DATE': 'Date',
|
||||
'TIME': 'Time',
|
||||
'DATETIME': 'DateTime',
|
||||
'TIMESTAMP': 'TIMESTAMP',
|
||||
'YEAR': 'Integer',
|
||||
# 字符串类型
|
||||
'CHAR': 'CHAR',
|
||||
'VARCHAR': 'String',
|
||||
'TINYTEXT': 'Text',
|
||||
'TEXT': 'Text',
|
||||
'MEDIUMTEXT': 'Text',
|
||||
'LONGTEXT': 'Text',
|
||||
'BINARY': 'BINARY',
|
||||
'VARBINARY': 'VARBINARY',
|
||||
'TINYBLOB': 'LargeBinary',
|
||||
'BLOB': 'LargeBinary',
|
||||
'MEDIUMBLOB': 'LargeBinary',
|
||||
'LONGBLOB': 'LargeBinary',
|
||||
# 枚举和集合类型
|
||||
'ENUM': 'Enum',
|
||||
'SET': 'String',
|
||||
# JSON 类型
|
||||
'JSON': 'JSON',
|
||||
# 空间数据类型(需要扩展支持,如 GeoAlchemy2)
|
||||
'GEOMETRY': 'Geometry', # 需要安装 geoalchemy2
|
||||
'POINT': 'Geometry',
|
||||
'LINESTRING': 'Geometry',
|
||||
'POLYGON': 'Geometry',
|
||||
'MULTIPOINT': 'Geometry',
|
||||
'MULTILINESTRING': 'Geometry',
|
||||
'MULTIPOLYGON': 'Geometry',
|
||||
'GEOMETRYCOLLECTION': 'Geometry',
|
||||
}
|
||||
)
|
||||
DB_TO_PYTHON_TYPE_MAPPING = (
|
||||
{
|
||||
'boolean': 'bool',
|
||||
'smallint': 'int',
|
||||
'integer': 'int',
|
||||
'bigint': 'int',
|
||||
'real': 'float',
|
||||
'double precision': 'float',
|
||||
'numeric': 'Decimal',
|
||||
'character varying': 'str',
|
||||
'character': 'str',
|
||||
'text': 'str',
|
||||
'bytea': 'bytes',
|
||||
'date': 'date',
|
||||
'time': 'time',
|
||||
'time with time zone': 'time',
|
||||
'time without time zone': 'time',
|
||||
'timestamp': 'datetime',
|
||||
'timestamp with time zone': 'datetime',
|
||||
'timestamp without time zone': 'datetime',
|
||||
'interval': 'timedelta',
|
||||
'json': 'dict',
|
||||
'jsonb': 'dict',
|
||||
'uuid': 'str',
|
||||
'inet': 'str',
|
||||
'cidr': 'str',
|
||||
'macaddr': 'str',
|
||||
'point': 'list',
|
||||
'line': 'list',
|
||||
'lseg': 'list',
|
||||
'box': 'list',
|
||||
'path': 'list',
|
||||
'polygon': 'list',
|
||||
'circle': 'list',
|
||||
'bit': 'int',
|
||||
'bit varying': 'int',
|
||||
'tsvector': 'str',
|
||||
'tsquery': 'str',
|
||||
'xml': 'str',
|
||||
'array': 'list',
|
||||
'composite': 'dict',
|
||||
'enum': 'str',
|
||||
'range': 'list',
|
||||
'money': 'Decimal',
|
||||
'pg_lsn': 'int',
|
||||
'txid_snapshot': 'str',
|
||||
'oid': 'int',
|
||||
'regproc': 'str',
|
||||
'regclass': 'str',
|
||||
'regtype': 'str',
|
||||
'regrole': 'str',
|
||||
'regnamespace': 'str',
|
||||
'int2vector': 'list',
|
||||
'oidvector': 'list',
|
||||
'pg_node_tree': 'str',
|
||||
}
|
||||
if DataBaseConfig.db_type == 'postgresql'
|
||||
else {
|
||||
# 数值类型
|
||||
'TINYINT': 'int',
|
||||
'SMALLINT': 'int',
|
||||
'MEDIUMINT': 'int',
|
||||
'INT': 'int',
|
||||
'INTEGER': 'int',
|
||||
'BIGINT': 'int',
|
||||
'FLOAT': 'float',
|
||||
'DOUBLE': 'float',
|
||||
'DECIMAL': 'Decimal',
|
||||
'BIT': 'int',
|
||||
# 日期和时间类型
|
||||
'DATE': 'date',
|
||||
'TIME': 'time',
|
||||
'DATETIME': 'datetime',
|
||||
'TIMESTAMP': 'datetime',
|
||||
'YEAR': 'int',
|
||||
# 字符串类型
|
||||
'CHAR': 'str',
|
||||
'VARCHAR': 'str',
|
||||
'TINYTEXT': 'str',
|
||||
'TEXT': 'str',
|
||||
'MEDIUMTEXT': 'str',
|
||||
'LONGTEXT': 'str',
|
||||
'BINARY': 'bytes',
|
||||
'VARBINARY': 'bytes',
|
||||
'TINYBLOB': 'bytes',
|
||||
'BLOB': 'bytes',
|
||||
'MEDIUMBLOB': 'bytes',
|
||||
'LONGBLOB': 'bytes',
|
||||
# 枚举和集合类型
|
||||
'ENUM': 'str',
|
||||
'SET': 'str',
|
||||
# JSON 类型
|
||||
'JSON': 'dict',
|
||||
# 空间数据类型(通常需要特殊处理)
|
||||
'GEOMETRY': 'bytes',
|
||||
'POINT': 'bytes',
|
||||
'LINESTRING': 'bytes',
|
||||
'POLYGON': 'bytes',
|
||||
'MULTIPOINT': 'bytes',
|
||||
'MULTILINESTRING': 'bytes',
|
||||
'MULTIPOLYGON': 'bytes',
|
||||
'GEOMETRYCOLLECTION': 'bytes',
|
||||
}
|
||||
)
|
||||