Initial commit: 投注游戏平台初始化
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
flv.js [](https://www.npmjs.com/package/flv.js)
|
||||
======
|
||||
An HTML5 Flash Video (FLV) Player written in pure JavaScript without Flash. LONG LIVE FLV!
|
||||
|
||||
This project relies on [Media Source Extensions][] to work.
|
||||
|
||||
**For FLV live stream playback, please consider [mpegts.js][] which is under active development.**
|
||||
|
||||
**This project will become rarely maintained.**
|
||||
|
||||
[mpegts.js]: https://github.com/xqq/mpegts.js
|
||||
## Overview
|
||||
flv.js works by transmuxing FLV file stream into ISO BMFF (Fragmented MP4) segments, followed by feeding mp4 segments into an HTML5 `<video>` element through [Media Source Extensions][] API.
|
||||
|
||||
[Media Source Extensions]: https://w3c.github.io/media-source/
|
||||
|
||||
## Demo
|
||||
[http://bilibili.github.io/flv.js/demo/](http://bilibili.github.io/flv.js/demo/)
|
||||
|
||||
## Features
|
||||
- FLV container with H.264 + AAC / MP3 codec playback
|
||||
- Multipart segmented video playback
|
||||
- HTTP FLV low latency live stream playback
|
||||
- FLV over WebSocket live stream playback
|
||||
- Compatible with Chrome, FireFox, Safari 10, IE11 and Edge
|
||||
- Extremely low overhead, and hardware accelerated by your browser!
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
npm install --save flv.js
|
||||
```
|
||||
|
||||
## Build
|
||||
```bash
|
||||
npm ci # install dependencies / dev-dependences
|
||||
npm run build:debug # debug version flv.js will be emitted to /dist
|
||||
npm run build # minimized release version flv.min.js will be emitted to /dist
|
||||
```
|
||||
|
||||
[cnpm](https://github.com/cnpm/cnpm) mirror is recommended if you are in Mainland China.
|
||||
|
||||
## CORS
|
||||
If you use standalone video server for FLV stream, `Access-Control-Allow-Origin` header must be configured correctly on video server for cross-origin resource fetching.
|
||||
|
||||
See [cors.md](docs/cors.md) for more details.
|
||||
|
||||
## Getting Started
|
||||
```html
|
||||
<script src="flv.min.js"></script>
|
||||
<video id="videoElement"></video>
|
||||
<script>
|
||||
if (flvjs.isSupported()) {
|
||||
var videoElement = document.getElementById('videoElement');
|
||||
var flvPlayer = flvjs.createPlayer({
|
||||
type: 'flv',
|
||||
url: 'http://example.com/flv/video.flv'
|
||||
});
|
||||
flvPlayer.attachMediaElement(videoElement);
|
||||
flvPlayer.load();
|
||||
flvPlayer.play();
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Limitations
|
||||
- MP3 audio codec is currently not working on IE11 / Edge
|
||||
- HTTP FLV live stream is not currently working on all browsers, see [livestream.md](docs/livestream.md)
|
||||
|
||||
## Multipart playback
|
||||
You only have to provide a playlist for `MediaDataSource`. See [multipart.md](docs/multipart.md)
|
||||
|
||||
## Livestream playback
|
||||
See [livestream.md](docs/livestream.md)
|
||||
|
||||
## API and Configuration
|
||||
See [api.md](docs/api.md)
|
||||
|
||||
## Debug
|
||||
```bash
|
||||
npm ci # install dependencies / dev-dependences
|
||||
npm run dev # watch file changes and build debug version on the fly
|
||||
```
|
||||
|
||||
## Design
|
||||
See [design.md](docs/design.md)
|
||||
|
||||
## License
|
||||
```
|
||||
Copyright (C) 2016 Bilibili. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2017 Dailymotion (http://www.dailymotion.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
src/remux/mp4-generator.js and src/demux/exp-golomb.ts implementation in this project
|
||||
are derived from the HLS library for video.js (https://github.com/videojs/videojs-contrib-hls)
|
||||
|
||||
That work is also covered by the Apache 2 License, following copyright:
|
||||
Copyright (c) 2013-2015 Brightcove
|
||||
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,442 @@
|
||||
[](https://npmjs.org/package/hls.js)
|
||||
[](https://www.npmjs.com/package/hls.js/v/canary)
|
||||
[](https://www.jsdelivr.com/package/npm/hls.js)
|
||||
[](https://app.saucelabs.com/u/robwalch)
|
||||
|
||||
[comment]: <> ([](https://saucelabs.com/u/robwalch))
|
||||
|
||||
# 
|
||||
|
||||
HLS.js is a JavaScript library that implements an [HTTP Live Streaming] client.
|
||||
It relies on [HTML5 video][] and [MediaSource Extensions][] for playback.
|
||||
|
||||
It works by transmuxing MPEG-2 Transport Stream and AAC/MP3 streams into ISO BMFF (MP4) fragments.
|
||||
Transmuxing is performed asynchronously using a [Web Worker] when available in the browser.
|
||||
HLS.js also supports HLS + fmp4, as announced during [WWDC2016](https://developer.apple.com/videos/play/wwdc2016/504/).
|
||||
|
||||
HLS.js works directly on top of a standard HTML`<video>` element.
|
||||
|
||||
HLS.js is written in [ECMAScript6] (`*.js`) and [TypeScript] (`*.ts`) (strongly typed superset of ES6), and transpiled in ECMAScript5 using [Babel](https://babeljs.io/) and the [TypeScript compiler].
|
||||
|
||||
[Webpack] is used to build the distro bundle and serve the local development environment.
|
||||
|
||||
[html5 video]: https://www.html5rocks.com/en/tutorials/video/basics/
|
||||
[mediasource extensions]: https://w3c.github.io/media-source/
|
||||
[http live streaming]: https://en.wikipedia.org/wiki/HTTP_Live_Streaming
|
||||
[web worker]: https://caniuse.com/#search=worker
|
||||
[ecmascript6]: https://github.com/ericdouglas/ES6-Learning#articles--tutorials
|
||||
[typescript]: https://www.typescriptlang.org/
|
||||
[typescript compiler]: https://www.typescriptlang.org/docs/handbook/compiler-options.html
|
||||
[webpack]: https://webpack.js.org/
|
||||
|
||||
## Features
|
||||
|
||||
- VOD & Live playlists
|
||||
- DVR support on Live playlists
|
||||
- Fragmented MP4 container
|
||||
- MPEG-2 TS container
|
||||
- ITU-T Rec. H.264 and ISO/IEC 14496-10 Elementary Stream
|
||||
- ISO/IEC 13818-7 ADTS AAC Elementary Stream
|
||||
- ISO/IEC 11172-3 / ISO/IEC 13818-3 (MPEG-1/2 Audio Layer III) Elementary Stream
|
||||
- Packetized metadata (ID3v2.3.0) Elementary Stream
|
||||
- AAC container (audio only streams)
|
||||
- MPEG Audio container (MPEG-1/2 Audio Layer III audio only streams)
|
||||
- Timed Metadata for HTTP Live Streaming (in ID3 format, carried in MPEG-2 TS)
|
||||
- AES-128 decryption
|
||||
- SAMPLE-AES decryption (only supported if using MPEG-2 TS container)
|
||||
- Encrypted media extensions (EME) support for DRM (digital rights management)
|
||||
- Widevine CDM (only tested with [shaka-packager](https://github.com/google/shaka-packager) test-stream on [the demo page](https://hls-js.netlify.app/demo/?src=https%3A%2F%2Fstorage.googleapis.com%2Fshaka-demo-assets%2Fangel-one-widevine-hls%2Fhls.m3u8&demoConfig=eyJlbmFibGVTdHJlYW1pbmciOnRydWUsImF1dG9SZWNvdmVyRXJyb3IiOnRydWUsInN0b3BPblN0YWxsIjpmYWxzZSwiZHVtcGZNUDQiOmZhbHNlLCJsZXZlbENhcHBpbmciOi0xLCJsaW1pdE1ldHJpY3MiOi0xfQ==))
|
||||
- CEA-608/708 captions
|
||||
- WebVTT subtitles
|
||||
- Alternate Audio Track Rendition (Master Playlist with Alternative Audio) for VoD and Live playlists
|
||||
- Adaptive streaming
|
||||
- Manual & Auto Quality Switching
|
||||
- 3 Quality Switching modes are available (controllable through API means)
|
||||
- Instant switching (immediate quality switch at current video position)
|
||||
- Smooth switching (quality switch for next loaded fragment)
|
||||
- Bandwidth conservative switching (quality switch change for next loaded fragment, without flushing the buffer)
|
||||
- In Auto-Quality mode, emergency switch down in case bandwidth is suddenly dropping to minimize buffering.
|
||||
- Accurate Seeking on VoD & Live (not limited to fragment or keyframe boundary)
|
||||
- Ability to seek in buffer and back buffer without redownloading segments
|
||||
- Built-in Analytics
|
||||
- All internal events can be monitored (Network Events, Video Events)
|
||||
- Playback session metrics are also exposed
|
||||
- Resilience to errors
|
||||
- Retry mechanism embedded in the library
|
||||
- Recovery actions can be triggered fix fatal media or network errors
|
||||
- [Redundant/Failover Playlists](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW22)
|
||||
|
||||
### Supported M3U8 tags
|
||||
|
||||
For details on the HLS format and these tags' meanings, see https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08
|
||||
|
||||
#### Manifest tags
|
||||
|
||||
- `#EXT-X-STREAM-INF:<attribute-list>`
|
||||
`<URI>`
|
||||
- `#EXT-X-MEDIA:<attribute-list>`
|
||||
- `#EXT-X-SESSION-DATA:<attribute-list>`
|
||||
|
||||
The following properties are added to their respective variants' attribute list but are not implemented in their selection and playback.
|
||||
|
||||
- `VIDEO-RANGE` and `HDCP-LEVEL` (See [#2489](https://github.com/video-dev/hls.js/issues/2489))
|
||||
|
||||
#### Playlist tags
|
||||
|
||||
- `#EXTM3U`
|
||||
- `#EXT-X-VERSION=<n>`
|
||||
- `#EXTINF:<duration>,[<title>]`
|
||||
- `#EXT-X-ENDLIST`
|
||||
- `#EXT-X-MEDIA-SEQUENCE=<n>`
|
||||
- `#EXT-X-TARGETDURATION=<n>`
|
||||
- `#EXT-X-DISCONTINUITY`
|
||||
- `#EXT-X-DISCONTINUITY-SEQUENCE=<n>`
|
||||
- `#EXT-X-BYTERANGE=<n>[@<o>]`
|
||||
- `#EXT-X-MAP:<attribute-list>`
|
||||
- `#EXT-X-KEY:<attribute-list>` (`METHOD=SAMPLE-AES` is only supports with MPEG-2 TS segments)
|
||||
- `#EXT-X-PROGRAM-DATE-TIME:<attribute-list>`
|
||||
- `#EXT-X-START:TIME-OFFSET=<n>`
|
||||
- `#EXT-X-SERVER-CONTROL:<attribute-list>`
|
||||
- `#EXT-X-PART-INF:PART-TARGET=<n>`
|
||||
- `#EXT-X-PART:<attribute-list>`
|
||||
- `#EXT-X-PRELOAD-HINT:<attribute-list>`
|
||||
- `#EXT-X-SKIP:<attribute-list>`
|
||||
- `#EXT-X-RENDITION-REPORT:<attribute-list>`
|
||||
|
||||
The following tags are added to their respective fragment's attribute list but are not implemented in streaming and playback.
|
||||
|
||||
- `#EXT-X-DATERANGE:<attribute-list>` (Not added to metadata TextTracks. See [#2218](https://github.com/video-dev/hls.js/issues/2218))
|
||||
- `#EXT-X-BITRATE` (Not used in ABR controller)
|
||||
- `#EXT-X-GAP` (Not implemented. See [#2940](https://github.com/video-dev/hls.js/issues/2940))
|
||||
|
||||
### Not Supported
|
||||
|
||||
For a complete list of issues, see ["Top priorities" in the Release Planning and Backlog project tab](https://github.com/video-dev/hls.js/projects/6). Codec support is dependent on the runtime environment (for example, not all browsers on the same OS support HEVC).
|
||||
|
||||
- CMAF CC support [#2623](https://github.com/video-dev/hls.js/issues/2623)
|
||||
- `Emsg` Inband Timed Metadata for FMP4 (ID3 within Emsgv1) in "metadata" TextTracks [#2360](https://github.com/video-dev/hls.js/issues/2360)
|
||||
- `#EXT-X-DATERANGE` in "metadata" TextTracks [#2218](https://github.com/video-dev/hls.js/issues/2218)
|
||||
- `#EXT-X-GAP` filling [#2940](https://github.com/video-dev/hls.js/issues/2940)
|
||||
- `#EXT-X-I-FRAME-STREAM-INF` I-frame Media Playlist files
|
||||
- `SAMPLE-AES` with fmp4, aac, mp3, vtt... segments (MPEG-2 TS only)
|
||||
- PlayReady and FairPlay DRM ( See [#3779](https://github.com/video-dev/hls.js/issues/2360) and [issues labeled DRM](https://github.com/video-dev/hls.js/issues?q=is%3Aissue+is%3Aopen+label%3ADRM))
|
||||
- Advanced variant selection based on runtime media capabilities (See issues labeled [`media-capabilities`](https://github.com/video-dev/hls.js/labels/media-capabilities))
|
||||
- MP3 elementary stream audio in IE and Edge (<=18) on Windows 10 (See [#1641](https://github.com/video-dev/hls.js/issues/1641) and [Microsoft answers forum](https://answers.microsoft.com/en-us/ie/forum/all/ie11-on-windows-10-cannot-play-hls-with-mp3/2da994b5-8dec-4ae9-9201-7d138ede49d9))
|
||||
|
||||
### Server-side-rendering (SSR) and `require` from a Node.js runtime
|
||||
|
||||
You can safely require this library in Node and **absolutely nothing will happen**. A dummy object is exported so that requiring the library does not throw an error. HLS.js is not instantiable in Node.js. See [#1841](https://github.com/video-dev/hls.js/pull/1841) for more details.
|
||||
|
||||
## Getting started with development
|
||||
|
||||
First, checkout the repository and install the required dependencies
|
||||
|
||||
```sh
|
||||
git clone https://github.com/video-dev/hls.js.git
|
||||
cd hls.js
|
||||
# After cloning or pulling from the repository, make sure all dependencies are up-to-date
|
||||
npm install ci
|
||||
# Run dev-server for demo page (recompiles on file-watch, but doesn't write to actual dist fs artifacts)
|
||||
npm run dev
|
||||
# After making changes run the sanity-check task to verify all checks before committing changes
|
||||
npm run sanity-check
|
||||
```
|
||||
|
||||
The dev server will host files on port 8000. Once started, the demo can be found running at http://localhost:8000/demo/.
|
||||
|
||||
Before submitting a PR, please see our [contribution guidelines](CONTRIBUTING.md).
|
||||
Join the discussion on Slack via [video-dev.org](https://video-dev.org) in #hlsjs for updates and questions about development.
|
||||
|
||||
### Build tasks
|
||||
|
||||
Build all flavors (suitable for prod-mode/CI):
|
||||
|
||||
```
|
||||
npm install ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
Only debug-mode artifacts:
|
||||
|
||||
```
|
||||
npm run build:debug
|
||||
```
|
||||
|
||||
Build and watch (customized dev setups where you'll want to host through another server than webpacks' - for example in a sub-module/project)
|
||||
|
||||
```
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
Only specific flavor (known configs are: debug, dist, light, light-dist, demo):
|
||||
|
||||
```
|
||||
npm run build -- --env dist # replace "dist" by other configuration name, see above ^
|
||||
```
|
||||
|
||||
Note: The "demo" config is always built.
|
||||
|
||||
**NOTE:** `hls.light.*.js` dist files do not include EME, subtitles, CMCD, or alternate-audio support. In addition,
|
||||
the following types are not available in the light build:
|
||||
|
||||
- `AudioStreamController`
|
||||
- `AudioTrackController`
|
||||
- `CuesInterface`
|
||||
- `EMEController`
|
||||
- `SubtitleStreamController`
|
||||
- `SubtitleTrackController`
|
||||
- `TimelineController`
|
||||
- `CmcdController`
|
||||
|
||||
### Linter (ESlint)
|
||||
|
||||
Run linter:
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Run linter with auto-fix mode:
|
||||
|
||||
```
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
Run linter with errors only (no warnings)
|
||||
|
||||
```
|
||||
npm run lint:quiet
|
||||
```
|
||||
|
||||
### Formatting Code
|
||||
|
||||
Run prettier to format code
|
||||
|
||||
```
|
||||
npm run prettier
|
||||
```
|
||||
|
||||
### Type Check
|
||||
|
||||
Run type-check to verify TypeScript types
|
||||
|
||||
```
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
### Automated tests (Mocha/Karma)
|
||||
|
||||
Run all tests at once:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
Run unit tests:
|
||||
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
Run unit tests in watch mode:
|
||||
|
||||
```
|
||||
npm run test:unit:watch
|
||||
```
|
||||
|
||||
Run functional (integration) tests:
|
||||
|
||||
```
|
||||
npm run test:func
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
An overview of this project's design, it's modules, events, and error handling can be found [here](/docs/design.md).
|
||||
|
||||
## API docs and usage guide
|
||||
|
||||
- [API and usage docs, with code examples](./docs/API.md)
|
||||
- [Auto-Generated API Docs (Latest Release)](https://hls-js.netlify.com/api-docs)
|
||||
- [Auto-Generated API Docs (Development Branch)](https://hls-js-dev.netlify.com/api-docs)
|
||||
|
||||
_Note you can access the docs for a particular version using "[https://github.com/video-dev/hls.js/tree/deployments](https://github.com/video-dev/hls.js/tree/deployments)"_
|
||||
|
||||
## Demo
|
||||
|
||||
### Latest Release
|
||||
|
||||
[https://hls-js.netlify.com/demo](https://hls-js.netlify.com/demo)
|
||||
|
||||
### Master
|
||||
|
||||
[https://hls-js-dev.netlify.com/demo](https://hls-js-dev.netlify.com/demo)
|
||||
|
||||
### Specific Version
|
||||
|
||||
Find the commit on [https://github.com/video-dev/hls.js/tree/deployments](https://github.com/video-dev/hls.js/tree/deployments).
|
||||
|
||||
[](https://www.netlify.com)
|
||||
|
||||
[](https://saucelabs.com)
|
||||
|
||||
## Compatibility
|
||||
|
||||
HLS.js is only compatible with browsers supporting MediaSource extensions (MSE) API with 'video/MP4' mime-type inputs.
|
||||
|
||||
HLS.js is supported on:
|
||||
|
||||
- Chrome 39+ for Android
|
||||
- Chrome 39+ for Desktop
|
||||
- Firefox 41+ for Android
|
||||
- Firefox 42+ for Desktop
|
||||
- IE11 for Windows 8.1+
|
||||
- Edge for Windows 10+
|
||||
- Safari 8+ for MacOS 10.10+
|
||||
- Safari for ipadOS 13+
|
||||
|
||||
A [Promise polyfill](https://github.com/taylorhakes/promise-polyfill) is required in browsers missing native promise support.
|
||||
|
||||
**Please note:** iOS Safari on iPhone does not support the MediaSource API. This includes all browsers on iOS as well as apps using UIWebView and WKWebView.
|
||||
|
||||
Safari browsers (iOS, iPadOS, and macOS) have built-in HLS support through the plain video "tag" source URL. See the example below (Using HLS.js) to run appropriate feature detection and choose between using HLS.js or natively built-in HLS support.
|
||||
|
||||
When a platform has neither MediaSource nor native HLS support, the browser cannot play HLS.
|
||||
|
||||
_Keep in mind that if the intention is to support HLS on multiple platforms, beyond those compatible with HLS.js, the HLS streams need to strictly follow the specifications of RFC8216, especially if apps, smart TVs, and set-top boxes are to be supported._
|
||||
|
||||
Find a support matrix of the MediaSource API here: https://developer.mozilla.org/en-US/docs/Web/API/MediaSource
|
||||
|
||||
## Using HLS.js
|
||||
|
||||
### Installation
|
||||
|
||||
Prepackaged builds are included [with each release](https://github.com/video-dev/hls.js/releases). Or install the hls.js as a dependency
|
||||
of your project:
|
||||
|
||||
```sh
|
||||
npm install --save hls.js
|
||||
```
|
||||
|
||||
A canary channel is also available if you prefer to work off the development branch (master):
|
||||
|
||||
```
|
||||
npm install hls.js@canary
|
||||
```
|
||||
|
||||
### Embedding HLS.js
|
||||
|
||||
Directly include dist/hls.js or dist/hls.min.js in a script tag on the page. This setup prioritizes HLS.js MSE playback over
|
||||
native browser support for HLS playback in HTMLMediaElements:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
<!-- Or if you want a more recent alpha version -->
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/hls.js@alpha"></script> -->
|
||||
<video id="video"></video>
|
||||
<script>
|
||||
var video = document.getElementById('video');
|
||||
var videoSrc = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
|
||||
if (Hls.isSupported()) {
|
||||
var hls = new Hls();
|
||||
hls.loadSource(videoSrc);
|
||||
hls.attachMedia(video);
|
||||
}
|
||||
// HLS.js is not supported on platforms that do not have Media Source
|
||||
// Extensions (MSE) enabled.
|
||||
//
|
||||
// When the browser has built-in HLS support (check using `canPlayType`),
|
||||
// we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video
|
||||
// element through the `src` property. This is using the built-in support
|
||||
// of the plain video element, without using HLS.js.
|
||||
//
|
||||
// Note: it would be more normal to wait on the 'canplay' event below however
|
||||
// on Safari (where you are most likely to find built-in HLS support) the
|
||||
// video.src URL must be on the user-driven white-list before a 'canplay'
|
||||
// event will be emitted; the last video event that can be reliably
|
||||
// listened-for when the URL is not on the white-list is 'loadedmetadata'.
|
||||
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = videoSrc;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Alternative setup
|
||||
|
||||
To check for native browser support first and then fallback to HLS.js, swap these conditionals. See [this comment](https://github.com/video-dev/hls.js/pull/2954#issuecomment-670021358) to understand some of the tradeoffs.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
<!-- Or if you want a more recent alpha version -->
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/hls.js@alpha"></script> -->
|
||||
<video id="video"></video>
|
||||
<script>
|
||||
var video = document.getElementById('video');
|
||||
var videoSrc = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
|
||||
//
|
||||
// First check for native browser HLS support
|
||||
//
|
||||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = videoSrc;
|
||||
//
|
||||
// If no native HLS support, check if HLS.js is supported
|
||||
//
|
||||
} else if (Hls.isSupported()) {
|
||||
var hls = new Hls();
|
||||
hls.loadSource(videoSrc);
|
||||
hls.attachMedia(video);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
For more embed and API examples see [docs/API.md](./docs/API.md).
|
||||
|
||||
## CORS
|
||||
|
||||
All HLS resources must be delivered with [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) permitting `GET` requests.
|
||||
|
||||
## Video Control
|
||||
|
||||
Video is controlled through HTML `<video>` element `HTMLVideoElement` methods, events and optional UI controls (`<video controls>`).
|
||||
|
||||
## Player Integration
|
||||
|
||||
The following players integrate HLS.js for HLS playback:
|
||||
|
||||
- [JW Player](https://www.jwplayer.com)
|
||||
- [Akamai Adaptive Media Player (AMP)](https://www.akamai.com/us/en/solutions/products/media-delivery/adaptive-media-player.jsp)
|
||||
- [Clappr](https://github.com/clappr/clappr)
|
||||
- [Flowplayer](https://www.flowplayer.org) through [flowplayer-hlsjs](https://github.com/flowplayer/flowplayer-hlsjs)
|
||||
- [MediaElement.js](https://www.mediaelementjs.com)
|
||||
- [Videojs](https://videojs.com) through [Videojs-hlsjs](https://github.com/benjipott/videojs-hlsjs)
|
||||
- [Videojs](https://videojs.com) through [videojs-hls.js](https://github.com/streamroot/videojs-hls.js). hls.js is integrated as a SourceHandler -- new feature in Video.js 5.
|
||||
- [Videojs](https://videojs.com) through [videojs-contrib-hls.js](https://github.com/Peer5/videojs-contrib-hls.js). Production ready plug-in with full fallback compatibility built-in.
|
||||
- [Fluid Player](https://www.fluidplayer.com)
|
||||
- [OpenPlayerJS](https://www.openplayerjs.com), as part of the [OpenPlayer project](https://github.com/openplayerjs)
|
||||
- [CDNBye](https://github.com/cdnbye/hlsjs-p2p-engine), a p2p engine for hls.js powered by WebRTC Datachannel.
|
||||
|
||||
### They use HLS.js in production!
|
||||
|
||||
| | | | |
|
||||
| :----------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| [<img src="https://i.cdn.turner.com/adultswim/big/img/global/adultswim.jpg" width="120">](https://www.adultswim.com/streams) | [<img src="https://avatars3.githubusercontent.com/u/5497190?s=200&v=4" width="120">](https://www.akamai.com) | [<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/Canal%2B.svg/2000px-Canal%2B.svg.png" width="120">](https://www.canalplus.fr) | [<img src="https://avatars2.githubusercontent.com/u/115313" width="120">](https://www.dailymotion.com) |
|
||||
| [<img src="https://user-images.githubusercontent.com/4006693/44003595-baff193c-9e8f-11e8-9848-7bb91563499f.png" width="120">](https://freshlive.tv) | [<img src="https://flowplayer.org/media/img/logo-blue.png" width="120">](https://flowplayer.com) | [<img src="https://avatars1.githubusercontent.com/u/12554082?s=240" width="120">](https://www.foxsports.com.au) | [<img src="https://cloud.githubusercontent.com/assets/244265/12556435/dfaceb48-c353-11e5-971b-2c4429725469.png" width="120">](https://www.globo.com) |
|
||||
| [<img src="https://images.gunosy.com/logo/gunosy_icon_company_logo.png" width="120">](https://gunosy.com) | [<img src="https://user-images.githubusercontent.com/1480052/35802840-f8e85b8a-0a71-11e8-8eb2-eee323e3f159.png" width="120">](https://www.gl-systemhaus.de/) | [<img src="https://cloud.githubusercontent.com/assets/6525783/20801836/700490de-b7ea-11e6-82bd-e249f91c7bae.jpg" width="120">](https://nettrek.de) | [<img src="https://cloud.githubusercontent.com/assets/244265/12556385/999aa884-c353-11e5-9102-79df54384498.png" width="120">](https://www.nytimes.com/) |
|
||||
| [<img src="https://cloud.githubusercontent.com/assets/1798553/20356424/ba158574-ac24-11e6-95e1-1ae591b11a0a.png" width="120">](https://www.peer5.com/) | [<img src="https://cloud.githubusercontent.com/assets/4909096/20925062/e26e6fc8-bbb4-11e6-99a5-d4762274a342.png" width="120">](https://www.qbrick.com) | [<img src="https://www.radiantmediaplayer.com/images/radiantmediaplayer-new-logo-640.jpg" width="120">](https://www.radiantmediaplayer.com/) | [<img src="https://www.rts.ch/hummingbird-static/images/logos/logo_marts.svg" width="120">](https://www.rts.ch) |
|
||||
| [<img src="https://cloud.githubusercontent.com/assets/12702747/19316434/0a3601de-9067-11e6-85e2-936b1cb099a0.png" width="120">](https://www.snapstream.com/) | [<img src="https://pamediagroup.com/wp-content/uploads/2019/05/StreamAMG-Logo-RGB.png" width="120">](https://www.streamamg.com/) | [<img src="https://streamsharkio.sa.metacdn.com/wp-content/uploads/2015/10/streamshark-dark.svg" width="120">](https://streamshark.io/) | [<img src="https://camo.githubusercontent.com/9580f10e9bfa8aa7fba52c5cb447bee0757e33da/68747470733a2f2f7777772e7461626c6f74762e636f6d2f7374617469632f696d616765732f7461626c6f5f6c6f676f2e706e67" width="120">](https://my.tablotv.com/) |
|
||||
| [<img src="https://user-images.githubusercontent.com/2803310/34083705-349c8fd0-e375-11e7-92a6-5c38509f4936.png" width="120">](https://www.streamroot.io/) | [<img src="https://vignette1.wikia.nocookie.net/tedtalks/images/c/c0/TED_logo.png/revision/20150915192527" width="120">](https://www.ted.com/) | [<img src="https://www.seeklogo.net/wp-content/uploads/2014/12/twitter-logo-vector-download.jpg" width="120">](https://twitter.com/) | [<img src="https://player.clevercast.com/img/clevercast.png" width="120">](https://www.clevercast.com) |
|
||||
| [<img src="https://player.mtvnservices.com/edge/hosted/Viacom_logo.svg" width="120">](https://www.viacom.com/) | [<img src="https://user-images.githubusercontent.com/1181974/29248959-efabc440-802d-11e7-8050-7c1f4ca6c607.png" width="120">](https://vk.com/) | [<img src="https://avatars0.githubusercontent.com/u/5090060?s=200&v=4" width="120">](https://www.jwplayer.com) | [<img src="https://staticftv-a.akamaihd.net/arches/francetv/default/img/og-image.jpg?20161007" width="120">](https://www.france.tv) |
|
||||
| [<img src="https://showmax.akamaized.net/e/logo/showmax_black.png" width="120">](https://tech.showmax.com) | [<img src="https://static3.1tv.ru/assets/web/logo-ac67852f1625b338f9d1fb96be089d03557d50bfc5790d5f48dc56799f59dec6.svg" width="120" height="120">](https://www.1tv.ru/) | [<img src="https://user-images.githubusercontent.com/1480052/40482633-c013ebce-5f55-11e8-96d5-b776415de0ce.png" width="120">](https://www.zdf.de) | [<img src="https://github.com/cdnbye/hlsjs-p2p-engine/blob/master/figs/cdnbye.png" width="120">](https://github.com/cdnbye/hlsjs-p2p-engine) |
|
||||
| [cdn77](https://streaming.cdn77.com/) | [<img src="https://avatars0.githubusercontent.com/u/7442371?s=200&v=4" width="120">](https://r7.com/) | [<img src="https://raw.githubusercontent.com/Novage/p2p-media-loader/gh-pages/images/p2pml-logo.png" width="120">](https://github.com/Novage/p2p-media-loader) | [<img src="https://avatars3.githubusercontent.com/u/45617200?s=400" width="120">](https://kayosports.com.au) |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/5279615?s=400&u=9771a216836c613f1edf4afe71cfc69d4c5657ed&v=4" width="120">](https://flosports.tv) | [<img src="https://www.logolynx.com/images/logolynx/c6/c67a2cb3ad33a82b5518f8ad8f124703.png" width="120">](https://global.axon.com/) | | |
|
||||
|
||||
## Chrome/Firefox integration
|
||||
|
||||
made by [gramk](https://github.com/gramk/chrome-hls), plays hls from address bar and m3u8 links
|
||||
|
||||
- Chrome [native-hls](https://chrome.google.com/webstore/detail/native-hls-playback/emnphkkblegpebimobpbekeedfgemhof)
|
||||
- Firefox [native-hls](https://addons.mozilla.org/en-US/firefox/addon/native_hls_playback/)
|
||||
|
||||
## License
|
||||
|
||||
HLS.js is released under [Apache 2.0 License](LICENSE)
|
||||
@@ -0,0 +1,72 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = global || self, global.ckplayerLanguage = factory());
|
||||
}(this, function () { 'use strict';
|
||||
/*
|
||||
* 功能:包含播放器用到的全部相关语言文字
|
||||
*/
|
||||
var language={
|
||||
play:'Play',
|
||||
pause:'Pause',
|
||||
refresh:'Refresh',
|
||||
full:'Fullscreen',
|
||||
exitFull:'Non-Fullscreen',
|
||||
webFull:'Web fullscreen',
|
||||
exitWebFull:'Non-Web fullscreen',
|
||||
theatre:'Theatre',
|
||||
exitTheatre:'Non-theatre',
|
||||
volume:'Volume:',
|
||||
muted:'Mute',
|
||||
exitmuted:'Unmute',
|
||||
seek:'Seek:',
|
||||
waiting:'Waiting',
|
||||
live:'Liveing',
|
||||
backLive:'Back live',
|
||||
lookBack:'Look back:',
|
||||
next:'Next episode',
|
||||
screenshot:'Screenshot',
|
||||
smallwindows:'Small windows',
|
||||
playbackrate:'Speed',
|
||||
playbackrateSuffix:' Speed',
|
||||
track:'Subtitle',
|
||||
noTrack:'No subtitle',
|
||||
definition:'Definition',
|
||||
switchTo:'Switched from:',
|
||||
closeTime:'The advertisement can be closed in {seconds} seconds',
|
||||
closeAd:'Close ad',
|
||||
second:'seconds',
|
||||
details:'View details',
|
||||
copy:'Copy',
|
||||
copySucceeded:'Copy succeeded, can be pasted!',
|
||||
smallwindowsOpen:'The small window function is turned on',
|
||||
screenshotStart:'Screenshot, please wait...',
|
||||
smallwindowsClose:'The small window function is turned off',
|
||||
screenshotClose:'Screenshot function is turned off',
|
||||
loopOpen:'Loop open',
|
||||
loopClose:'Loop close',
|
||||
close:'Close',
|
||||
down:'Down',
|
||||
p50:'50%',
|
||||
p75:'75%',
|
||||
p100:'100%',
|
||||
timeScheduleAdjust:{
|
||||
prohibit:'No dragging',
|
||||
prohibitBackOff:'No repeat viewing',
|
||||
prohibitForward:'Fast forward prohibited',
|
||||
prohibitLookBack:'Some content is forbidden to play',
|
||||
prohibitForwardNotViewed:'Disable playback of parts not viewed'
|
||||
},
|
||||
error:{
|
||||
noMessage:'Unknown error',
|
||||
supportVideoError:'The browser version is too low. It is recommended to replace it with another browser',
|
||||
videoTypeError:'This browser does not support playing this video. It is recommended to replace it with another browser',
|
||||
loadingFailed:'Loading failed',
|
||||
emptied:'An error occurred while loading the frequency file',
|
||||
screenshot:'Screenshot failed',
|
||||
ajax:'Ajax data request error',
|
||||
noVideoContainer:'No video container'
|
||||
}
|
||||
};
|
||||
return language;
|
||||
}))
|
||||
@@ -0,0 +1,72 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = global || self, global.ckplayerLanguage = factory());
|
||||
}(this, function () { 'use strict';
|
||||
/*
|
||||
* 功能:包含播放器用到的全部相关语言文字
|
||||
*/
|
||||
var language={
|
||||
play:'播放',
|
||||
pause:'暂停',
|
||||
refresh:'重播',
|
||||
full:'全屏',
|
||||
exitFull:'退出全屏',
|
||||
webFull:'页面全屏',
|
||||
exitWebFull:'退出页面全屏',
|
||||
theatre:'剧场模式',
|
||||
exitTheatre:'退出剧场模式',
|
||||
volume:'音量:',
|
||||
muted:'静音',
|
||||
exitmuted:'恢复音量',
|
||||
seek:'seek:',
|
||||
waiting:'缓冲',
|
||||
live:'直播中',
|
||||
backLive:'返回直播',
|
||||
lookBack:'回看:',
|
||||
next:'下一集',
|
||||
screenshot:'视频截图',
|
||||
smallwindows:'小窗口播放功能',
|
||||
playbackrate:'倍速',
|
||||
playbackrateSuffix:'倍',
|
||||
track:'字幕',
|
||||
noTrack:'无字幕',
|
||||
definition:'清晰度',
|
||||
switchTo:'切换成:',
|
||||
closeTime:'{seconds}秒后可关闭广告',
|
||||
closeAd:'关闭广告',
|
||||
second:'秒',
|
||||
details:'查看详情',
|
||||
copy:'复制',
|
||||
copySucceeded:'复制成功,可贴粘!',
|
||||
smallwindowsOpen:'小窗口功能已开启',
|
||||
smallwindowsClose:'小窗口功能已关闭',
|
||||
screenshotStart:'截图中,请稍候...',
|
||||
screenshotClose:'截图功能已关闭',
|
||||
loopOpen:'循环播放',
|
||||
loopClose:'已关闭循环播放',
|
||||
close:'关闭',
|
||||
down:'下载',
|
||||
p50:'50%',
|
||||
p75:'75%',
|
||||
p100:'100%',
|
||||
timeScheduleAdjust:{
|
||||
prohibit:'视频禁止拖动',
|
||||
prohibitBackOff:'视频禁止重复观看',
|
||||
prohibitForward:'视频禁止快进',
|
||||
prohibitLookBack:'视频禁止播放部分内容',
|
||||
prohibitForwardNotViewed:'视频禁止播放未观看的部分'
|
||||
},
|
||||
error:{
|
||||
noMessage:'未知错误',
|
||||
supportVideoError:'该浏览器版本太低,建议更换成其它浏览器',
|
||||
videoTypeError:'该浏览器不支持播放该视频,建议更换成其它浏览器',
|
||||
loadingFailed:'加载失败',
|
||||
emptied:'视频文件加载过程中出现错误',
|
||||
screenshot:'视频截图失败',
|
||||
ajax:'Ajax数据请求错误',
|
||||
noVideoContainer:'未找到放置视频的容器'
|
||||
}
|
||||
};
|
||||
return language;
|
||||
}))
|
||||
@@ -0,0 +1,72 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = global || self, global.ckplayerLanguage = factory());
|
||||
}(this, function () { 'use strict';
|
||||
/*
|
||||
*功能:包含播放機用到的全部相關語言文字
|
||||
*/
|
||||
var language = {
|
||||
play: '播放',
|
||||
pause: '暫停',
|
||||
refresh: '重播',
|
||||
full: '全屏',
|
||||
exitFull: '退出全屏',
|
||||
webFull: '頁面全屏',
|
||||
exitWebFull: '退出頁面全屏',
|
||||
theatre: '劇場模式',
|
||||
exitTheatre: '退出劇場模式',
|
||||
volume: '音量:',
|
||||
muted: '靜音',
|
||||
exitmuted: '恢復音量',
|
||||
seek: 'seek:',
|
||||
waiting: '緩衝',
|
||||
live: '直播中',
|
||||
backLive: '返回直播',
|
||||
lookBack: '回看:',
|
||||
next: '下一集',
|
||||
screenshot: '視頻截圖',
|
||||
smallwindows: '小視窗播放功能',
|
||||
playbackrate: '倍速',
|
||||
playbackrateSuffix: '倍',
|
||||
track: '字幕',
|
||||
noTrack: '無字幕',
|
||||
definition: '清晰度',
|
||||
switchTo: '切換成:',
|
||||
closeTime: '{seconds}秒後可關閉廣告',
|
||||
closeAd: '關閉廣告',
|
||||
second: '秒',
|
||||
details: '查看詳情',
|
||||
copy: '複製',
|
||||
copySucceeded: '複製成功,可貼粘!',
|
||||
smallwindowsOpen: '小視窗功能已開啟',
|
||||
smallwindowsClose: '小視窗功能已關閉',
|
||||
screenshotStart: '截圖中,請稍候…',
|
||||
screenshotClose: '截圖功能已關閉',
|
||||
loopOpen: '迴圈播放',
|
||||
loopClose: '已關閉迴圈播放',
|
||||
close: '關閉',
|
||||
down: '下載',
|
||||
p50: '50%',
|
||||
p75: '75%',
|
||||
p100: '100%',
|
||||
timeScheduleAdjust: {
|
||||
prohibit: '視頻禁止拖動',
|
||||
prohibitBackOff: '視頻禁止重複觀看',
|
||||
prohibitForward: '視頻禁止快進',
|
||||
prohibitLookBack: '視頻禁止播放部分內容',
|
||||
prohibitForwardNotViewed: '視頻禁止播放未觀看的部分'
|
||||
},
|
||||
error: {
|
||||
noMessage: '未知錯誤',
|
||||
supportVideoError: '該流覽器版本太低,建議更換成其它瀏覽器',
|
||||
videoTypeError: '該瀏覽器不支持播放該視頻,建議更換成其它瀏覽器',
|
||||
loadingFailed: '加載失敗',
|
||||
emptied: '視頻檔案加載過程中出現錯誤',
|
||||
screenshot: '視頻截圖失敗',
|
||||
ajax: 'Ajax數據請求錯誤',
|
||||
noVideoContainer: '未找到放置視頻的容器'
|
||||
}
|
||||
};
|
||||
return language;
|
||||
}))
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,77 @@
|
||||
/* ===== PK10 白色主题设计系统 ===== */
|
||||
:root{
|
||||
--bg:#F5F5F5;--card:#fff;--primary:#1E90FF;--primary-dark:#1873CC;
|
||||
--danger:#FF4444;--success:#4CAF50;--warn:#FF9800;
|
||||
--text:#333;--text2:#666;--text3:#999;--border:#E5E5E5;
|
||||
--shadow:0 2px 12px rgba(0,0,0,.08);--radius:16px;--radius-sm:12px;
|
||||
--c1:#e74c3c;--c2:#3498db;--c3:#2ecc71;--c4:#f39c12;--c5:#9b59b6;
|
||||
--c6:#1abc9c;--c7:#e67e22;--c8:#e91e63;--c9:#00bcd4;--c10:#8bc34a;
|
||||
--nav-h:60px;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:var(--bg);color:var(--text);font-family:-apple-system,'PingFang SC','Helvetica Neue',sans-serif;overflow-x:hidden;-webkit-font-smoothing:antialiased}
|
||||
a{color:var(--primary);text-decoration:none}
|
||||
|
||||
/* 卡片 */
|
||||
.card{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);padding:16px}
|
||||
|
||||
/* 按钮 */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:24px;font-weight:600;cursor:pointer;transition:all .2s;font-size:14px;padding:10px 24px}
|
||||
.btn-primary{background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:#fff}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(30,144,255,.35)}
|
||||
.btn-primary:active{transform:translateY(0)}
|
||||
.btn-outline{background:#fff;color:var(--primary);border:1px solid var(--primary)}
|
||||
.btn-danger{background:linear-gradient(135deg,var(--danger),#CC3333);color:#fff}
|
||||
.btn-sm{padding:6px 16px;font-size:12px;border-radius:20px}
|
||||
.btn-block{width:100%;display:flex}
|
||||
|
||||
/* 输入框 */
|
||||
.input{width:100%;padding:12px 16px;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:14px;color:var(--text);outline:none;transition:border .2s}
|
||||
.input:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(30,144,255,.1)}
|
||||
.input::placeholder{color:var(--text3)}
|
||||
|
||||
/* 赛车颜色球 */
|
||||
.car{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;color:#fff;flex-shrink:0}
|
||||
.c1{background:var(--c1)}.c2{background:var(--c2)}.c3{background:var(--c3)}.c4{background:var(--c4)}.c5{background:var(--c5)}
|
||||
.c6{background:var(--c6)}.c7{background:var(--c7)}.c8{background:var(--c8)}.c9{background:var(--c9)}.c10{background:var(--c10)}
|
||||
|
||||
/* 底部导航 */
|
||||
.nav-bottom{position:fixed;bottom:0;left:0;right:0;height:60px;background:#fff;border-top:1px solid #F0F0F0;z-index:1000;display:flex}
|
||||
.nav-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;cursor:pointer;color:#999;transition:color .3s;text-decoration:none}
|
||||
.nav-item.active{color:#1E90FF}
|
||||
.nav-item svg{width:24px;height:24px}
|
||||
.nav-item span{font-size:12px}
|
||||
|
||||
/* 标签页 */
|
||||
.tabs{display:flex;background:#fff;border-bottom:1px solid var(--border)}
|
||||
.tab{flex:1;text-align:center;padding:10px 0;font-size:13px;color:var(--text3);cursor:pointer;border-bottom:2px solid transparent;transition:all .2s}
|
||||
.tab.active{color:var(--primary);border-bottom-color:var(--primary);font-weight:600}
|
||||
|
||||
/* 投注按钮 */
|
||||
.bet-btn{background:#fff;border:1px solid var(--border);border-radius:8px;padding:8px 4px;text-align:center;cursor:pointer;transition:all .15s;user-select:none}
|
||||
.bet-btn:active{transform:scale(.97)}
|
||||
.bet-btn.selected{border-color:var(--primary);background:rgba(30,144,255,.08);box-shadow:0 0 0 2px rgba(30,144,255,.2)}
|
||||
|
||||
/* 筹码 */
|
||||
.chip{cursor:pointer;transition:all .15s;border-radius:50%;padding:8px 0;text-align:center;font-weight:700;font-size:12px;color:#fff}
|
||||
.chip:active{transform:scale(.95)}
|
||||
.chip.active{box-shadow:0 0 0 3px var(--primary),0 0 12px rgba(30,144,255,.3)}
|
||||
|
||||
/* Toast */
|
||||
.toast{position:fixed;top:80px;left:50%;transform:translateX(-50%);padding:10px 24px;border-radius:24px;font-size:13px;z-index:999;color:#fff;box-shadow:0 4px 16px rgba(0,0,0,.15)}
|
||||
.toast-ok{background:var(--success)}.toast-err{background:var(--danger)}
|
||||
|
||||
/* 工具类 */
|
||||
.text-primary{color:var(--primary)}.text-danger{color:var(--danger)}.text-success{color:var(--success)}.text-muted{color:var(--text3)}
|
||||
.text-center{text-align:center}.text-sm{font-size:12px}.text-xs{font-size:11px}.text-lg{font-size:18px}.text-xl{font-size:24px}
|
||||
.font-bold{font-weight:700}.font-mono{font-family:'SF Mono',monospace}
|
||||
.flex{display:flex}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}
|
||||
.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}
|
||||
.p-3{padding:12px}.p-4{padding:16px}.px-4{padding-left:16px;padding-right:16px}.py-2{padding-top:8px;padding-bottom:8px}
|
||||
.mt-2{margin-top:8px}.mt-3{margin-top:12px}.mt-4{margin-top:16px}.mb-2{margin-bottom:8px}.mb-3{margin-bottom:12px}
|
||||
.w-full{width:100%}.max-w-md{max-width:448px}.mx-auto{margin-left:auto;margin-right:auto}
|
||||
.hidden{display:none!important}.overflow-y-auto{overflow-y:auto}
|
||||
.rounded-xl{border-radius:var(--radius)}.rounded-lg{border-radius:var(--radius-sm)}
|
||||
.grid{display:grid}.grid-cols-2{grid-template-columns:repeat(2,1fr)}.grid-cols-5{grid-template-columns:repeat(5,1fr)}
|
||||
.space-y-2>*+*{margin-top:8px}.space-y-3>*+*{margin-top:12px}.space-y-4>*+*{margin-top:16px}
|
||||
.pb-nav{padding-bottom:calc(var(--nav-h) + 16px)}
|
||||
@@ -0,0 +1,747 @@
|
||||
/* 全局样式 */
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.ja-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes ja-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ja-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 模态窗口 */
|
||||
.ja-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ja-up-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(var(--vh, 1vh) * 85);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
padding: 24px;
|
||||
animation: ja-fade-in 0.3s ease-out;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ja-modal-content {
|
||||
border-radius: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ja-modal-active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ja-modal-close,
|
||||
.ja-img-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.ja-modal-close img {
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.ja-img-close {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 标签页 */
|
||||
.ja-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #E2E8F0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.ja-tab-button {
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #64748B;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ja-tab-button:hover {
|
||||
color: #1E40AF;
|
||||
}
|
||||
|
||||
.ja-tab-button-active {
|
||||
color: #165DFF;
|
||||
border-bottom-color: #165DFF;
|
||||
}
|
||||
|
||||
.ja-tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ja-tab-content-active {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
border: 1px solid #E2E8F0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
.ja-upload-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
border: 2px dashed #E2E8F0;
|
||||
border-radius: 12px;
|
||||
background-color: #F8FAFC;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ja-upload-label:hover {
|
||||
border-color: #94A3B8;
|
||||
background-color: #F1F5F9;
|
||||
}
|
||||
|
||||
.ja-upload-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: #94A3B8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ja-upload-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ja-upload-desc {
|
||||
font-size: 14px;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
/* 参数设置 */
|
||||
.ja-params {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ja-param-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ja-param-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ja-range-input {
|
||||
-webkit-appearance: none;
|
||||
height: 6px;
|
||||
width: 88%;
|
||||
border-radius: 3px;
|
||||
background: #E2E8F0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ja-range-span {
|
||||
width: 8%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ja-range-input::-webkit-slider-thumb,
|
||||
.ja-range-input::-moz-range-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #165DFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ja-text-input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.ja-text-input:focus {
|
||||
border-color: #165DFF;
|
||||
}
|
||||
|
||||
.ja-text-input::placeholder {
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
/* 上传按钮 */
|
||||
.ja-button {
|
||||
width: 270px;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ja-button-primary {
|
||||
background-color: #165DFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#ja-upload-btn>* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ja-button-primary:hover {
|
||||
background-color: #0E42D2;
|
||||
}
|
||||
|
||||
.ja-button-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: #fff;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.ja-erro-icon{
|
||||
width: 60px;
|
||||
height: auto;
|
||||
}
|
||||
/* 上传结果 */
|
||||
.ja-upload-results {
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ja-upload-item {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #eaedf0;
|
||||
}
|
||||
|
||||
.ja-upload-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ja-upload-filename {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.ja-upload-status {
|
||||
font-size: 12px;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.ja-upload-success {
|
||||
font-size: 12px;
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.ja-upload-failure {
|
||||
font-size: 12px;
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.ja-upload-progress-container {
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ja-upload-progress-bar {
|
||||
height: 2px;
|
||||
background-color: #165DFF;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.ja-upload-details {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ja-detail-item {
|
||||
display: flex;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.ja-detail-label {
|
||||
font-size: 12px;
|
||||
color: #64748B;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.ja-detail-value {
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.ja-upload-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ja-action-link,
|
||||
.ja-copy-url {
|
||||
font-size: 12px;
|
||||
color: #165DFF;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ja-action-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ja-copy-url {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ja-upload-error {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #FEF2F2;
|
||||
border: 1px solid #FEE2E2;
|
||||
}
|
||||
|
||||
.ja-error-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ja-error-filename {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.ja-error-status {
|
||||
font-size: 12px;
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.ja-error-message {
|
||||
font-size: 12px;
|
||||
color: #B91C1C;
|
||||
}
|
||||
|
||||
/* 媒体网格 */
|
||||
#ja-media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
#ja-media-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
#ja-media-grid {
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
#ja-media-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.ja-media-item {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.ja-media-item:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.ja-item-thumbnail {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
}
|
||||
|
||||
.ja-thumbnail-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.ja-media-item:hover .ja-thumbnail-image {
|
||||
transform: scale(1.05);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.ja-item-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ja-media-item:hover .ja-item-overlay {
|
||||
opacity: 1;
|
||||
height: 103%
|
||||
}
|
||||
|
||||
.ja-preview-button,
|
||||
.ja-copy-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ja-item-thumbnail:hover .ja-preview-button,
|
||||
.ja-item-thumbnail:hover .ja-copy-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ja-action-icon {
|
||||
pointer-events: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.ja-preview-button,
|
||||
.ja-copy-button {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ja-item-info {
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ja-item-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
margin-bottom: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ja-item-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ja-item-size,
|
||||
.ja-item-dimensions {
|
||||
font-size: 12px;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
/* 分页控件 */
|
||||
#ja-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.ja-page-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 4px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
background-color: #F8FAFC;
|
||||
border: 1px solid #E2E8F0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ja-page-button:hover:not(.ja-disabled):not(.ja-active) {
|
||||
background-color: #F1F5F9;
|
||||
}
|
||||
|
||||
.ja-page-button.ja-active {
|
||||
background-color: #165DFF;
|
||||
color: #fff;
|
||||
border-color: #165DFF;
|
||||
}
|
||||
|
||||
.ja-page-button.ja-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ja-page-ellipsis {
|
||||
margin: 0 4px;
|
||||
font-size: 14px;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.ja-page-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 加载/空状态/错误状态 */
|
||||
.ja-loading-container,
|
||||
.ja-empty-container,
|
||||
.ja-error-container {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.ja-loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ja-loading-circle {
|
||||
stroke: #165DFF;
|
||||
animation: ja-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.ja-loading-text {
|
||||
font-size: 14px;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.ja-empty-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: #E2E8F0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ja-empty-text {
|
||||
font-size: 14px;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.ja-error-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: #F87171;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ja-error-text {
|
||||
font-size: 14px;
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
/* 提示消息 */
|
||||
.ja-toast {
|
||||
position: fixed;
|
||||
bottom: 50%;
|
||||
left: 50%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 20px);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
/* 预览图片 */
|
||||
.ja-image-preview-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.ja-modal-content {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
max-width: 80%;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ja-modal-image {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.ja-close-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.ja-toast-info {
|
||||
background-color: #165DFF;
|
||||
}
|
||||
|
||||
.ja-toast-success {
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
.ja-toast-error {
|
||||
background-color: #EF4444;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.uploading-icon {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.loading-icon {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 2px solid rgba(26, 115, 232, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #1a73e8;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#ja-media-grid{
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 566 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,480 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 初始化各功能模块
|
||||
initMobileSidebar();
|
||||
initSubmenuNavigation();
|
||||
highlightActiveMenu();
|
||||
initAjaxContentLoader();
|
||||
initProfileEditModal();
|
||||
|
||||
// 触发一次resize事件用于响应式布局初始化
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 显示消息提示
|
||||
* @param {string} text - 提示内容
|
||||
* @param {string} type - 提示类型(success/error/warning/info)
|
||||
*/
|
||||
|
||||
function showMessage(message, type = 'success') {
|
||||
const styles = {success: 'bg-green-500', error: 'bg-red-500', warning: 'bg-yellow-500', info: 'bg-blue-500' };
|
||||
const icons = {success: 'fa-check-circle', error: 'fa-exclamation-circle', warning: 'fa-exclamation-triangle', info: 'fa-info-circle' };
|
||||
const baseClasses = ['fixed', 'top-4', 'right-4', 'z-50', 'px-4', 'py-3', 'rounded-lg', 'shadow-lg', 'text-white', 'flex', 'items-center', 'gap-2', 'max-w-sm', 'transition-transform', 'duration-300' ];
|
||||
let note = document.getElementById('notification');
|
||||
if (!note) {
|
||||
note = document.createElement('div');
|
||||
note.id = 'notification';
|
||||
note.classList.add(...baseClasses);
|
||||
document.body.appendChild(note);
|
||||
}
|
||||
note.style.transform = 'translateX(calc(100% + 20px))';
|
||||
note.className = '';
|
||||
note.classList.add(...baseClasses, styles[type]);
|
||||
note.innerHTML = `<i class="fas ${icons[type]}"></i><span style="word-wrap: break-word; flex: 1; max-width: calc(100% - 24px);">${message}</span>`;
|
||||
setTimeout(() => {
|
||||
note.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
clearTimeout(note.timer);
|
||||
note.timer = setTimeout(() => {
|
||||
note.style.transform = 'translateX(calc(100% + 20px))';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示确认弹窗
|
||||
* @param {string} text - 提示内容
|
||||
*/
|
||||
function showConfirm(title, msg) {
|
||||
return new Promise(resolve => {
|
||||
const mask = document.createElement("div");
|
||||
mask.className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50";
|
||||
mask.innerHTML = `
|
||||
<div class="bg-white rounded-lg shadow-lg w-64 text-center overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800">${title}</h3>
|
||||
</div>
|
||||
<div class="px-4 py-5">
|
||||
<p class="text-gray-600">${msg}</p>
|
||||
</div>
|
||||
<div class="px-4 py-3 flex justify-center gap-3">
|
||||
<button class="px-4 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"> 取消 </button>
|
||||
<button class="px-4 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"> 确定 </button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mask);
|
||||
const [cancelBtn, okBtn] = mask.querySelectorAll("button");
|
||||
cancelBtn.onclick = () => { mask.remove(); resolve(false); };
|
||||
okBtn.onclick = () => { mask.remove(); resolve(true); };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制内容
|
||||
* @param {string} text - 复制的内容
|
||||
* @param {string} type - 内容类型
|
||||
*/
|
||||
function copyToClipboard(text, typeName = '内容') {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showMessage(`${typeName} 复制成功`, 'success');
|
||||
}).catch(err => {
|
||||
console.error(`${typeName} 复制失败:`, err);
|
||||
showMessage(`${typeName} 复制失败,请手动复制`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端侧边栏菜单切换功能
|
||||
*/
|
||||
function initMobileSidebar() {
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
// 元素不存在则不初始化
|
||||
if (!mobileMenuButton || !sidebar || !sidebarOverlay) return;
|
||||
|
||||
// 打开侧边栏
|
||||
const openSidebar = () => {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
sidebar.classList.add('translate-x-0');
|
||||
sidebarOverlay.classList.remove('hidden');
|
||||
document.body.classList.add('overflow-hidden');
|
||||
};
|
||||
|
||||
// 关闭侧边栏
|
||||
const closeSidebar = () => {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
sidebar.classList.remove('translate-x-0');
|
||||
sidebarOverlay.classList.add('hidden');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
};
|
||||
|
||||
// 切换侧边栏状态
|
||||
const toggleSidebar = () => {
|
||||
sidebar.classList.contains('translate-x-0') ? closeSidebar() : openSidebar();
|
||||
};
|
||||
|
||||
// 绑定事件
|
||||
mobileMenuButton.addEventListener('click', toggleSidebar);
|
||||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
/**
|
||||
* 子菜单折叠/展开功能
|
||||
*/
|
||||
function initSubmenuNavigation() {
|
||||
// 关闭所有子菜单
|
||||
const closeAllSubmenus = () => {
|
||||
document.querySelectorAll('.submenu').forEach(submenu => {
|
||||
submenu.classList.add('hidden');
|
||||
});
|
||||
document.querySelectorAll('.menu-parent .fa-chevron-down').forEach(icon => {
|
||||
icon.classList.remove('rotate-180');
|
||||
});
|
||||
};
|
||||
|
||||
// 切换单个子菜单
|
||||
const toggleSubmenu = (parentElement) => {
|
||||
const submenu = parentElement.nextElementSibling;
|
||||
const icon = parentElement.querySelector('.fa-chevron-down');
|
||||
const isCurrentlyOpen = submenu && !submenu.classList.contains('hidden');
|
||||
|
||||
closeAllSubmenus();
|
||||
|
||||
if (!isCurrentlyOpen && submenu) {
|
||||
submenu.classList.remove('hidden');
|
||||
icon?.classList.add('rotate-180');
|
||||
}
|
||||
};
|
||||
|
||||
// 绑定父菜单点击事件
|
||||
document.querySelectorAll('.menu-parent').forEach(parent => {
|
||||
parent.addEventListener('click', (e) => {
|
||||
// 仅处理菜单本身的点击(不影响内部链接)
|
||||
if (e.target.tagName.toLowerCase() === 'a' || e.target.closest('a') === parent) {
|
||||
e.preventDefault();
|
||||
toggleSubmenu(parent);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮当前页面对应的菜单
|
||||
*/
|
||||
function highlightActiveMenu() {
|
||||
const currentUrl = window.location.pathname;
|
||||
|
||||
// 移除所有菜单的高亮状态
|
||||
document.querySelectorAll('.main-menu-item, .submenu-item').forEach(item => {
|
||||
item.classList.remove('bg-primary/10', 'text-primary', 'font-medium');
|
||||
});
|
||||
|
||||
// 优先高亮子菜单
|
||||
let submenuHighlighted = false;
|
||||
document.querySelectorAll('.submenu-item').forEach(item => {
|
||||
if (item.getAttribute('href') === currentUrl) {
|
||||
item.classList.add('bg-primary/10', 'text-primary', 'font-medium');
|
||||
submenuHighlighted = true;
|
||||
|
||||
// 高亮父菜单并展开子菜单
|
||||
const parentItem = item.closest('.menu-group').querySelector('.menu-parent');
|
||||
if (parentItem) {
|
||||
parentItem.classList.add('bg-primary/10', 'text-primary', 'font-medium');
|
||||
const submenu = parentItem.nextElementSibling;
|
||||
if (submenu?.classList.contains('submenu')) {
|
||||
submenu.classList.remove('hidden');
|
||||
parentItem.querySelector('.fa-chevron-down')?.classList.add('rotate-180');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 子菜单未匹配时,尝试高亮主菜单
|
||||
if (!submenuHighlighted) {
|
||||
document.querySelectorAll('.main-menu-item').forEach(item => {
|
||||
if (item.getAttribute('href') === currentUrl) {
|
||||
item.classList.add('bg-primary/10', 'text-primary', 'font-medium');
|
||||
const submenu = item.nextElementSibling;
|
||||
if (submenu?.classList.contains('submenu')) {
|
||||
submenu.classList.remove('hidden');
|
||||
item.querySelector('.fa-chevron-down')?.classList.add('rotate-180');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化插件内容的Ajax加载功能
|
||||
*/
|
||||
function initAjaxContentLoader() {
|
||||
// 通过Ajax加载内容
|
||||
const loadContent = (url) => {
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`加载失败: HTTP ${response.status}`);
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
const mainContent = document.getElementById('main-content');
|
||||
if (mainContent) {
|
||||
mainContent.innerHTML = html;
|
||||
highlightActiveMenu();
|
||||
|
||||
// 触发子页面加载完成事件
|
||||
window.dispatchEvent(new CustomEvent('subpageLoaded', { detail: { url } }));
|
||||
|
||||
// 移动端自动关闭侧边栏
|
||||
if (window.innerWidth < 768) {
|
||||
document.getElementById('mobile-menu-button')?.click();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('内容加载失败:', error);
|
||||
showMessage('页面加载失败,请重试', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
// 绑定菜单点击事件(通过Ajax加载内容)
|
||||
document.querySelectorAll('.plugin-menu-link, .plugin-submenu-link').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
// 有子菜单的父项不加载内容
|
||||
if (link.classList.contains('menu-parent') && link.nextElementSibling?.classList.contains('submenu')) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const url = link.getAttribute('href');
|
||||
if (url) {
|
||||
loadContent(url);
|
||||
history.pushState(null, '', url); // 更新浏览器地址栏
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听浏览器前进/后退事件
|
||||
window.addEventListener('popstate', () => {
|
||||
loadContent(window.location.pathname);
|
||||
});
|
||||
|
||||
// 监听子页面加载完成事件
|
||||
window.addEventListener('subpageLoaded', (e) => {
|
||||
console.log('子页面加载完成:', e.detail.url);
|
||||
// 此处注释掉了自动刷新,避免循环刷新
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化个人资料编辑弹窗
|
||||
*/
|
||||
function initProfileEditModal() {
|
||||
// 获取DOM元素
|
||||
const profileModal = document.getElementById('profileModal');
|
||||
const profileModalBackdrop = document.getElementById('profileModalBackdrop');
|
||||
const openProfileBtn = document.getElementById('profile');
|
||||
const closeProfileModal = document.getElementById('closeProfileModal');
|
||||
const cancelProfileBtn = document.getElementById('cancelProfileBtn');
|
||||
const profileForm = document.getElementById('profileForm');
|
||||
const userIdInput = document.getElementById('profileUserId');
|
||||
|
||||
// 元素不完整则不初始化
|
||||
if (!profileModal || !profileForm || !userIdInput) return;
|
||||
|
||||
const userId = userIdInput.value;
|
||||
|
||||
// 从服务器加载用户信息
|
||||
const loadUserProfile = async () => {
|
||||
try {
|
||||
const response = await fetch(`/admin/users/${userId}`);
|
||||
if (!response.ok) throw new Error('获取用户信息失败');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
// 填充邮箱字段
|
||||
const emailInput = document.getElementById('profileEmail');
|
||||
if (emailInput) emailInput.value = data.data.email || '';
|
||||
const roleDisplay = document.getElementById('userRoleDisplay');
|
||||
if (roleDisplay) {
|
||||
// 从接口数据中获取角色,转换为中文显示
|
||||
const role = data.data.role || 'user';
|
||||
roleDisplay.textContent = role === 'admin' ? '管理员' : '普通用户';
|
||||
}
|
||||
document.getElementById('userRoleInput').value = data.data.role;
|
||||
|
||||
// 3. 填充状态(新增逻辑)
|
||||
const statusDisplay = document.getElementById('userStatusDisplay');
|
||||
if (statusDisplay) {
|
||||
const rawStatus = data.data.status;
|
||||
const isEnabled = rawStatus == 1 || rawStatus == '1' || rawStatus == true;
|
||||
document.getElementById('userStatusInput').value = data.data.status;
|
||||
// 根据判断结果设置文本和样式
|
||||
const statusText = isEnabled ? '已启用' : '已禁用';
|
||||
const statusClass = isEnabled
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800';
|
||||
|
||||
// 渲染状态标签
|
||||
statusDisplay.innerHTML = `
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClass}">
|
||||
${statusText}
|
||||
</span>
|
||||
`;
|
||||
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载个人资料失败:', error);
|
||||
showMessage('加载个人资料失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openProfileModal = () => {
|
||||
profileModal.classList.remove('invisible', 'opacity-0');
|
||||
const modalContent = profileModal.querySelector('div');
|
||||
if (modalContent) {
|
||||
modalContent.classList.remove('scale-95');
|
||||
modalContent.classList.add('scale-100');
|
||||
}
|
||||
document.body.style.overflow = 'hidden';
|
||||
loadUserProfile(); // 加载用户数据
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeProfileModalFunc = () => {
|
||||
profileModal.classList.add('invisible', 'opacity-0');
|
||||
const modalContent = profileModal.querySelector('div');
|
||||
if (modalContent) {
|
||||
modalContent.classList.remove('scale-100');
|
||||
modalContent.classList.add('scale-95');
|
||||
}
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
|
||||
// 提交表单处理
|
||||
const submitProfileForm = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = profileForm.querySelector('button[type="submit"]');
|
||||
if (!submitBtn) return;
|
||||
|
||||
const originalText = submitBtn.innerHTML;
|
||||
// 禁用按钮并显示加载状态
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 保存中...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(profileForm);
|
||||
const response = await fetch('/admin/users/update', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage('个人资料更新成功');
|
||||
closeProfileModalFunc();
|
||||
|
||||
// 更新页面显示的用户名
|
||||
const newUsername = document.getElementById('profileUsername')?.value;
|
||||
if (newUsername) {
|
||||
|
||||
if (targetUsername) {
|
||||
const usernameElements = Array.from(document.querySelectorAll('span'))
|
||||
.filter(span => {
|
||||
return span.textContent.trim().includes(targetUsername.trim());
|
||||
});
|
||||
|
||||
usernameElements.forEach(el => {
|
||||
el.textContent = newUsername;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || '更新失败');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(error.message, 'error');
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
}
|
||||
};
|
||||
|
||||
// 绑定事件
|
||||
if (openProfileBtn) {
|
||||
openProfileBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
openProfileModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (closeProfileModal) closeProfileModal.addEventListener('click', closeProfileModalFunc);
|
||||
if (cancelProfileBtn) cancelProfileBtn.addEventListener('click', closeProfileModalFunc);
|
||||
if (profileModalBackdrop) profileModalBackdrop.addEventListener('click', closeProfileModalFunc);
|
||||
|
||||
profileForm.addEventListener('submit', submitProfileForm);
|
||||
|
||||
// ESC键关闭弹窗
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !profileModal.classList.contains('invisible')) {
|
||||
closeProfileModalFunc();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取元素(仅在元素存在时绑定事件)
|
||||
const contactLink = document.getElementById('contactSupport');
|
||||
const popup = document.getElementById('supportPopup');
|
||||
const closeBtn = document.getElementById('closePopup');
|
||||
|
||||
// 打开弹出层(仅在元素存在时)
|
||||
if (contactLink && popup) {
|
||||
contactLink.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
popup.classList.remove('hidden');
|
||||
// 添加淡入动画效果
|
||||
setTimeout(() => {
|
||||
const popupDiv = popup.querySelector('div');
|
||||
if (popupDiv) {
|
||||
popupDiv.classList.add('scale-100');
|
||||
popupDiv.classList.remove('scale-95');
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭弹出层
|
||||
function closePopup() {
|
||||
if (popup) {
|
||||
const popupDiv = popup.querySelector('div');
|
||||
if (popupDiv) {
|
||||
popupDiv.classList.add('scale-95');
|
||||
popupDiv.classList.remove('scale-100');
|
||||
}
|
||||
setTimeout(() => {
|
||||
popup.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定关闭按钮(仅在元素存在时)
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', closePopup);
|
||||
}
|
||||
|
||||
// 点击外部关闭(仅在元素存在时)
|
||||
if (popup) {
|
||||
popup.addEventListener('click', function(e) {
|
||||
if (e.target === popup) {
|
||||
closePopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,852 @@
|
||||
(function() {
|
||||
|
||||
// 常量定义 - 类名移除avif并添加ja-前缀
|
||||
const CLOSE_ICON = ` <img src="https://yanxuan.nosdn.127.net/0c64ce12c71cf276504cb2e15164d4ff.png"width="25px"> `;
|
||||
const RESET_ICON = ` <img src="https://yanxuan.nosdn.127.net/8296ea39d8c548ed320f79a483756cd9.jpg"width="25px" > `;
|
||||
const PREVIEW_ICON = ` <img class="ja-action-icon" src="https://yanxuan.nosdn.127.net/520b793c329df6349a25404efefbd0f4.png"/> `;
|
||||
const COPY_ICON = ` <img class="ja-action-icon" src="https://yanxuan.nosdn.127.net/0732b6e6d1f249dad11ac7d004af5964.png"/> `;
|
||||
const UP_ICON = ` <img class="ja-button-icon" src="https://yanxuan.nosdn.127.net/94494a4dc561ae21157f50764ddf035e.png"/> `;
|
||||
const ERRO_ICON = ` <img class="ja-erro-icon" src="https://yanxuan.nosdn.127.net/74777259ff515f056e8fc340df28fcd7.png"/> `;
|
||||
|
||||
// 基础配置
|
||||
const scriptBaseUrl = getScriptBaseUrl();
|
||||
const API_CONFIG = {
|
||||
listEndpoint: scriptBaseUrl + '/admin/images/list',
|
||||
uploadEndpoint: scriptBaseUrl + '/admin/images/upload'
|
||||
};
|
||||
|
||||
// 组件状态
|
||||
const state = {
|
||||
targetInputId: null,
|
||||
currentPage: 1,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
mediaItems: [],
|
||||
isLoading: false,
|
||||
activeTab: 'gallery'
|
||||
};
|
||||
let closeBtn = null;
|
||||
let resetBtn = null;
|
||||
|
||||
// 获取脚本基础URL
|
||||
function getScriptBaseUrl() {
|
||||
const script = document.currentScript;
|
||||
if (!script || !script.src) {
|
||||
console.warn('无法获取当前脚本URL,使用相对路径');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 使用URL对象解析完整路径
|
||||
const url = new URL(script.src);
|
||||
// 拼接 协议 + 域名 + 端口(如果有)
|
||||
const domain = `${url.protocol}//${url.host}`;
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(0)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function isImageFile(file) {
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif', 'avif'];
|
||||
return imageExtensions.includes(extension) || /image\/.*/.test(file.type);
|
||||
}
|
||||
|
||||
// 样式和布局相关
|
||||
function createStyles() {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = scriptBaseUrl + '/Static/css/controller.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// 防止地址栏遮挡
|
||||
function setVh() {
|
||||
document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`);
|
||||
}
|
||||
|
||||
// 注册事件监听
|
||||
window.addEventListener('resize', setVh);
|
||||
window.addEventListener('orientationchange', setVh);
|
||||
|
||||
// UI 创建函数 - 所有ID和类名添加ja-前缀并移除avif
|
||||
function createUI() {
|
||||
// 动态插入 CSS,禁止顶部下拉刷新
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
html, body {
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
`;
|
||||
|
||||
// 创建基础容器
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.id = 'ja-uploader';
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'ja-modal';
|
||||
modal.setAttribute('role', 'dialog');
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'ja-up-modal-content';
|
||||
|
||||
// 创建关闭按钮
|
||||
closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'ja-modal-close';
|
||||
closeBtn.innerHTML = `${CLOSE_ICON}`;
|
||||
|
||||
|
||||
// 创建重置按钮
|
||||
resetBtn = document.createElement('button');
|
||||
resetBtn.className = 'ja-img-close';
|
||||
resetBtn.setAttribute('data-action', 'reset');
|
||||
resetBtn.innerHTML = `${RESET_ICON}`;
|
||||
|
||||
// 标签页导航
|
||||
const tabs = document.createElement('div');
|
||||
tabs.className = 'ja-tabs';
|
||||
|
||||
const galleryTab = document.createElement('button');
|
||||
galleryTab.className = 'ja-tab-button ja-tab-button-active';
|
||||
galleryTab.dataset.tab = 'gallery';
|
||||
galleryTab.textContent = '图库';
|
||||
|
||||
const uploadTab = document.createElement('button');
|
||||
uploadTab.className = 'ja-tab-button';
|
||||
uploadTab.dataset.tab = 'upload';
|
||||
uploadTab.textContent = '上传';
|
||||
|
||||
// 图库内容
|
||||
const galleryContent = document.createElement('div');
|
||||
galleryContent.className = 'ja-tab-content ja-tab-content-active';
|
||||
galleryContent.dataset.tab = 'gallery';
|
||||
|
||||
const mediaGrid = document.createElement('div');
|
||||
mediaGrid.className = 'mb-2';
|
||||
mediaGrid.id = 'ja-media-grid';
|
||||
|
||||
// 上传内容
|
||||
const uploadContent = document.createElement('div');
|
||||
uploadContent.className = 'ja-tab-content';
|
||||
uploadContent.dataset.tab = 'upload';
|
||||
|
||||
// 上传参数
|
||||
const params = document.createElement('div');
|
||||
params.className = 'ja-params';
|
||||
|
||||
const qualityLabel = document.createElement('label');
|
||||
qualityLabel.className = 'ja-param-label';
|
||||
qualityLabel.innerHTML = `
|
||||
<span class="ja-param-title">质量设置 (1-100)</span>
|
||||
<div class="ja-range-wrapper">
|
||||
<input type="range" id="ja-quality" min="1" max="100" value="60" class="ja-range-input">
|
||||
<span id="ja-quality-value" class="ja-range-span">60</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const widthLabel = document.createElement('label');
|
||||
widthLabel.className = 'ja-param-label';
|
||||
widthLabel.innerHTML = `
|
||||
<span class="ja-param-title">转换宽度</span>
|
||||
<input type="number" id="ja-width" placeholder="留空为自动" class="ja-text-input">
|
||||
`;
|
||||
|
||||
const heightLabel = document.createElement('label');
|
||||
heightLabel.className = 'ja-param-label';
|
||||
heightLabel.innerHTML = `
|
||||
<span class="ja-param-title">转换高度</span>
|
||||
<input type="number" id="ja-height" placeholder="留空为自动" class="ja-text-input">
|
||||
`;
|
||||
|
||||
// 上传控件
|
||||
const uploadInput = document.createElement('input');
|
||||
uploadInput.type = 'file';
|
||||
uploadInput.id = 'ja-upload-input';
|
||||
uploadInput.multiple = true;
|
||||
uploadInput.accept = 'image/*';
|
||||
uploadInput.className = 'ja-hidden';
|
||||
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.id = 'ja-upload-btn';
|
||||
uploadBtn.className = 'ja-button ja-button-primary';
|
||||
uploadBtn.innerHTML = `
|
||||
<div id="ja-upload-svg"> ${UP_ICON} </div>
|
||||
<div id="ja-upload-status">开始上传</div>
|
||||
`;
|
||||
|
||||
const uploadResults = document.createElement('div');
|
||||
uploadResults.id = 'ja-upload-results';
|
||||
uploadResults.className = 'ja-upload-results';
|
||||
|
||||
// 加载更多按钮
|
||||
const loadMoreBtn = document.createElement('button');
|
||||
loadMoreBtn.id = 'ja-load-more';
|
||||
loadMoreBtn.className = 'ja-load-more ja-button ja-button-secondary mt-4';
|
||||
loadMoreBtn.innerHTML = `
|
||||
<i class="fa fa-refresh mr-2"></i> 加载更多
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
|
||||
// 统一建立父子关系
|
||||
// 标签页按钮
|
||||
tabs.appendChild(galleryTab);
|
||||
tabs.appendChild(uploadTab);
|
||||
|
||||
// 图库内容
|
||||
galleryContent.appendChild(mediaGrid);
|
||||
galleryContent.appendChild(loadMoreBtn);
|
||||
|
||||
// 上传参数
|
||||
params.appendChild(qualityLabel);
|
||||
params.appendChild(widthLabel);
|
||||
params.appendChild(heightLabel);
|
||||
|
||||
// 上传内容
|
||||
uploadContent.appendChild(params);
|
||||
uploadContent.appendChild(uploadBtn);
|
||||
uploadContent.appendChild(uploadInput);
|
||||
uploadContent.appendChild(uploadResults);
|
||||
|
||||
// 主内容区域
|
||||
content.appendChild(closeBtn);
|
||||
content.appendChild(resetBtn);
|
||||
content.appendChild(tabs);
|
||||
content.appendChild(galleryContent);
|
||||
content.appendChild(uploadContent);
|
||||
|
||||
// 模态框组装
|
||||
modal.appendChild(content);
|
||||
wrapper.appendChild(modal);
|
||||
|
||||
// 添加到文档
|
||||
document.body.appendChild(wrapper);
|
||||
}
|
||||
function updateButtonVisibility(showReset) {
|
||||
if (showReset) {
|
||||
closeBtn.style.display = 'none';
|
||||
resetBtn.style.display = 'block';
|
||||
} else {
|
||||
closeBtn.style.display = 'block';
|
||||
resetBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 存储表单状态到本地存储 - 键名移除avif
|
||||
function saveFormState(width, height) {
|
||||
localStorage.setItem('ja-FormState', JSON.stringify({ width, height }));
|
||||
}
|
||||
|
||||
// 从本地存储获取表单状态 - 键名移除avif
|
||||
function loadFormState() {
|
||||
const state = localStorage.getItem('ja-FormState');
|
||||
return state ? JSON.parse(state) : { width: '', height: '' };
|
||||
}
|
||||
|
||||
// 设置事件监听 - 选择器同步更新
|
||||
function setupEventListeners() {
|
||||
const qualityInput = document.getElementById('ja-quality');
|
||||
const qualityValue = document.getElementById('ja-quality-value');
|
||||
|
||||
if (qualityInput && qualityValue) {
|
||||
qualityInput.addEventListener('input', function() {
|
||||
qualityValue.textContent = this.value;
|
||||
});
|
||||
}
|
||||
|
||||
// 统一处理所有点击事件
|
||||
document.querySelector('#ja-uploader').addEventListener('click', (e) => {
|
||||
const target = e.target;
|
||||
|
||||
// 1. 模态框背景点击(关闭模态框)
|
||||
if (target === document.querySelector('.ja-modal')) {
|
||||
toggleModal();
|
||||
}
|
||||
//
|
||||
else if (target === document.querySelector('.ja-modal-close')) {
|
||||
toggleModal();
|
||||
}
|
||||
// 2. 标签页切换
|
||||
else if (target.classList.contains('ja-tab-button')) {
|
||||
const tab = target.dataset.tab;
|
||||
switchTab(tab);
|
||||
|
||||
// 切换到图库时重新获取列表
|
||||
if (tab === 'gallery') {
|
||||
fetchMediaList();
|
||||
}
|
||||
}
|
||||
// 3. 预览按钮
|
||||
else if (target.classList.contains('ja-preview-button')) {
|
||||
const url = target.dataset.url;
|
||||
previewFile(url);
|
||||
}
|
||||
|
||||
// 4. 填充按钮
|
||||
else if (target.classList.contains('ja-copy-button')) {
|
||||
const url = target.dataset.url;
|
||||
urlToPage(url);
|
||||
}
|
||||
|
||||
// 5. 上传按钮
|
||||
else if (target.id === 'ja-upload-btn') {
|
||||
document.getElementById('ja-upload-input').click();
|
||||
}
|
||||
|
||||
// 6. 上传结果中的关闭按钮
|
||||
else if (target.closest('.ja-img-close')) {
|
||||
resetUploadForm();
|
||||
}
|
||||
});
|
||||
|
||||
// 文件选择事件
|
||||
document.getElementById('ja-upload-input').addEventListener('change', handleFileSelect);
|
||||
|
||||
// 加载更多按钮点击事件
|
||||
document.getElementById('ja-load-more').addEventListener('click', loadMoreImages);
|
||||
}
|
||||
|
||||
// 核心功能函数:初始化图库
|
||||
function Gallery(inputId) {
|
||||
const targetInput = document.getElementById(inputId);
|
||||
if (!targetInput) {
|
||||
showToast('输入框不存在', 'error');
|
||||
return;
|
||||
}
|
||||
state.targetInputId = inputId;
|
||||
toggleModal();
|
||||
}
|
||||
|
||||
// 切换模态窗口
|
||||
function toggleModal() {
|
||||
fetchMediaList();
|
||||
const modal = document.querySelector('.ja-modal');
|
||||
modal.classList.toggle('ja-modal-active');
|
||||
setVh();
|
||||
if (!modal.classList.contains('ja-modal-active')) {
|
||||
resetUploadForm();
|
||||
}
|
||||
resetUploadForm();
|
||||
}
|
||||
|
||||
|
||||
function resetUploadForm() {
|
||||
// 从本地存储加载表单状态
|
||||
const { width, height } = loadFormState();
|
||||
|
||||
// 清空上传结果
|
||||
const uploadResults = document.getElementById('ja-upload-results');
|
||||
if (uploadResults) uploadResults.innerHTML = '';
|
||||
|
||||
// 清空文件选择
|
||||
const uploadInput = document.getElementById('ja-upload-input');
|
||||
if (uploadInput) uploadInput.value = '';
|
||||
|
||||
// 重置质量滑块
|
||||
const qualityInput = document.getElementById('ja-quality');
|
||||
if (qualityInput) qualityInput.value = 60;
|
||||
|
||||
// 填充宽度和高度
|
||||
const widthInput = document.getElementById('ja-width');
|
||||
const heightInput = document.getElementById('ja-height');
|
||||
if (widthInput) widthInput.value = width ;
|
||||
if (heightInput) heightInput.value = height ;
|
||||
}
|
||||
|
||||
// 切换标签
|
||||
function switchTab(tab) {
|
||||
state.activeTab = tab;
|
||||
|
||||
document.querySelectorAll('.ja-tab-button').forEach(btn => {
|
||||
btn.classList.toggle('ja-tab-button-active', btn.dataset.tab === tab);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.ja-tab-content').forEach(content => {
|
||||
content.classList.toggle('ja-tab-content-active', content.dataset.tab === tab);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取图片列表
|
||||
function fetchMediaList() {
|
||||
if (state.isLoading) return;
|
||||
state.isLoading = true;
|
||||
|
||||
const grid = document.getElementById('ja-media-grid');
|
||||
const loadMoreBtn = document.getElementById('ja-load-more');
|
||||
const isInitialLoad = state.currentPage === 1;
|
||||
|
||||
// 首次加载显示加载状态
|
||||
if (isInitialLoad) {
|
||||
grid.innerHTML = `
|
||||
<div class="ja-loading-container">
|
||||
<i class="loading-icon"></i>
|
||||
<p class="ja-loading-text">加载中...</p>
|
||||
</div>
|
||||
`;
|
||||
// 初始加载时隐藏加载更多按钮
|
||||
loadMoreBtn.style.display = 'none';
|
||||
} else {
|
||||
// 非首次加载时显示加载状态
|
||||
const loadingIndicator = document.createElement('div');
|
||||
loadingIndicator.className = 'ja-loading-container';
|
||||
loadingIndicator.innerHTML = `
|
||||
<i class="loading-icon"></i>
|
||||
<p class="ja-loading-text">加载中...</p>
|
||||
`;
|
||||
grid.appendChild(loadingIndicator);
|
||||
}
|
||||
|
||||
const itemsPerPage = 20;
|
||||
const params = new URLSearchParams({
|
||||
page: state.currentPage,
|
||||
limit: itemsPerPage
|
||||
});
|
||||
|
||||
fetch(`${API_CONFIG.listEndpoint}?${params}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
state.currentPage = data.page;
|
||||
state.totalItems = data.total;
|
||||
state.totalPages = Math.ceil(data.total / itemsPerPage);
|
||||
|
||||
// 合并数据
|
||||
const previousItemCount = state.mediaItems.length;
|
||||
state.mediaItems = isInitialLoad ?
|
||||
data.data :
|
||||
[...state.mediaItems, ...data.data];
|
||||
|
||||
renderMediaGrid();
|
||||
|
||||
// 显示加载更多按钮
|
||||
loadMoreBtn.style.display = 'block';
|
||||
|
||||
// 控制加载更多按钮显示
|
||||
if (state.currentPage >= state.totalPages) {
|
||||
loadMoreBtn.innerHTML = `
|
||||
<i class="fa fa-check mr-2"></i> 没有更多图片了
|
||||
`;
|
||||
loadMoreBtn.disabled = true;
|
||||
loadMoreBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
} else {
|
||||
loadMoreBtn.innerHTML = `
|
||||
<i class="fa fa-refresh mr-2"></i> 加载更多
|
||||
`;
|
||||
loadMoreBtn.disabled = false;
|
||||
loadMoreBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
|
||||
// 恢复滚动位置
|
||||
if (!isInitialLoad) {
|
||||
const newItemsCount = state.mediaItems.length - previousItemCount;
|
||||
const itemHeight = 150;
|
||||
const newContentHeight = newItemsCount * itemHeight;
|
||||
grid.scrollTop += newContentHeight;
|
||||
}
|
||||
} else {
|
||||
// 错误处理
|
||||
grid.innerHTML = `
|
||||
<div class="ja-error-container">
|
||||
${ERRO_ICON}
|
||||
<p class="ja-error-text">${data.message}</p>
|
||||
</div>
|
||||
`;
|
||||
loadMoreBtn.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取图片列表失败:', error);
|
||||
grid.innerHTML = `
|
||||
<div class="ja-error-container">
|
||||
${ERRO_ICON}
|
||||
<p class="ja-error-text">网络错误,请重试</p>
|
||||
</div>
|
||||
`;
|
||||
loadMoreBtn.style.display = 'none';
|
||||
})
|
||||
.finally(() => {
|
||||
state.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 加载更多函数
|
||||
function loadMoreImages() {
|
||||
if (state.isLoading || state.currentPage >= state.totalPages) return;
|
||||
|
||||
const grid = document.getElementById('ja-media-grid');
|
||||
state.currentPage++;
|
||||
|
||||
// 临时禁用滚动事件
|
||||
grid.style.overflowY = 'hidden';
|
||||
|
||||
fetchMediaList();
|
||||
|
||||
// 恢复滚动
|
||||
setTimeout(() => {
|
||||
grid.style.overflowY = 'auto';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 渲染图片网格
|
||||
function renderMediaGrid() {
|
||||
const grid = document.getElementById('ja-media-grid');
|
||||
const itemsToRender = state.mediaItems;
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (itemsToRender.length === 0) {
|
||||
// 空状态显示
|
||||
grid.innerHTML = `
|
||||
<div class="ja-empty-container">
|
||||
${ERRO_ICON}
|
||||
<p class="ja-empty-text">没有找到图片</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常渲染图片列表
|
||||
itemsToRender.forEach(item => {
|
||||
const itemElement = document.createElement('div');
|
||||
itemElement.className = 'ja-media-item';
|
||||
itemElement.innerHTML = `
|
||||
<div class="ja-item-thumbnail">
|
||||
<img src="${item.url}" alt="${item.name}" class="ja-thumbnail-image" loading="lazy">
|
||||
<div class="ja-item-overlay">
|
||||
<button class="ja-copy-button" data-url="${item.url}"> ${COPY_ICON} </button>
|
||||
<button class="ja-preview-button" data-url="${item.url}"> ${PREVIEW_ICON} </button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ja-item-info">
|
||||
<div class="ja-item-name">${item.name}</div>
|
||||
<div class="ja-item-details">
|
||||
<span class="ja-item-size">${formatFileSize(item.size)}</span>
|
||||
<span class="ja-item-dimensions">${item.width}*${item.height}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(itemElement);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 处理文件选择
|
||||
function handleFileSelect(e) {
|
||||
const files = e.target.files;
|
||||
if (!files.length) return;
|
||||
|
||||
const validFiles = [];
|
||||
const invalidFiles = [];
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
if (!isImageFile(file)) {
|
||||
invalidFiles.push({ file, reason: '不支持的文件类型,仅支持图片' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
invalidFiles.push({ file, reason: '文件过大,最大支持10MB' });
|
||||
return;
|
||||
}
|
||||
|
||||
validFiles.push(file);
|
||||
});
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
const uploadResults = document.getElementById('ja-upload-results');
|
||||
invalidFiles.forEach(({ file, reason }) => {
|
||||
const errorItem = document.createElement('div');
|
||||
errorItem.className = 'ja-upload-error';
|
||||
errorItem.innerHTML = `
|
||||
<div class="ja-error-header">
|
||||
<span class="ja-error-filename"></span>
|
||||
<span class="ja-error-status">错误</span>
|
||||
</div>
|
||||
<div class="ja-error-message">${reason}</div>
|
||||
`;
|
||||
uploadResults.appendChild(errorItem);
|
||||
});
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
uploadFiles(validFiles);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
function uploadFiles(files) {
|
||||
console.log('uploadEndpoint:', API_CONFIG.uploadEndpoint);
|
||||
|
||||
const quality = document.getElementById('ja-quality').value;
|
||||
const width = document.getElementById('ja-width').value;
|
||||
const height = document.getElementById('ja-height').value;
|
||||
const uploadResults = document.getElementById('ja-upload-results');
|
||||
saveFormState(width, height);
|
||||
uploadResults.innerHTML = '';
|
||||
|
||||
files.forEach(file => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('quality', quality);
|
||||
if (width) formData.append('width', width);
|
||||
if (height) formData.append('height', height);
|
||||
|
||||
const progressItem = document.createElement('div');
|
||||
progressItem.className = 'ja-upload-item';
|
||||
|
||||
uploadResults.appendChild(progressItem);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', API_CONFIG.uploadEndpoint, true);
|
||||
|
||||
// 设置超时时间
|
||||
xhr.timeout = 120000; // 120秒超时
|
||||
|
||||
// 超时处理
|
||||
xhr.ontimeout = function() {
|
||||
updateButtonVisibility(true);
|
||||
resetUploadButton();
|
||||
handleUploadError(file, '等待返回超时,已在后台处理,稍后到图片列表中查看;如果没有则是后台处理失败;大部分问题是图片过大', progressItem);
|
||||
};
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
const ja = window.ja;
|
||||
progressItem.style.width = `${percentComplete}%`;
|
||||
if (ja.statusText) {
|
||||
ja.statusText.textContent = `上传中 ${Math.round(percentComplete)}%`;
|
||||
ja.upsvg.innerHTML = '<i class="uploading-icon"></i>';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onload = function() {
|
||||
// 重置上传按钮状态
|
||||
resetUploadButton();
|
||||
|
||||
// 处理成功响应
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
handleUploadSuccess(response, progressItem);
|
||||
} catch (error) {
|
||||
console.error('解析响应失败:', error);
|
||||
console.log('原始响应:', xhr.responseText);
|
||||
|
||||
// 检查是否是HTML响应
|
||||
if (xhr.responseText.includes('<html') || xhr.responseText.includes('DOCTYPE')) {
|
||||
handleUploadError(file, '服务器返回了无效响应,请检查服务器配置', progressItem);
|
||||
} else {
|
||||
handleUploadError(file, `解析服务器响应失败: ${error.message}`, progressItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理错误响应
|
||||
else {
|
||||
updateButtonVisibility(true);
|
||||
try {
|
||||
const responseData = JSON.parse(xhr.responseText);
|
||||
console.log('错误响应:', responseData);
|
||||
handleUploadError(file, `${responseData.message || '服务器错误'}`, progressItem);
|
||||
} catch (parseError) {
|
||||
console.error('解析错误响应失败:', parseError);
|
||||
console.log('原始错误响应:', xhr.responseText);
|
||||
|
||||
// 检查是否是HTML响应
|
||||
if (xhr.responseText.includes('<html') || xhr.responseText.includes('DOCTYPE')) {
|
||||
handleUploadError(file, `服务器错误 (${xhr.status}): 请检查服务器配置`, progressItem);
|
||||
} else {
|
||||
handleUploadError(file, `服务器错误 (${xhr.status}): ${parseError.message}`, progressItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
updateButtonVisibility(true);
|
||||
resetUploadButton();
|
||||
handleUploadError(file, '网络错误,请重试', progressItem);
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
// 重置上传按钮状态的辅助函数
|
||||
function resetUploadButton() {
|
||||
const ja = window.ja;
|
||||
if (ja.statusText) {
|
||||
ja.statusText.textContent = `开始上传`;
|
||||
ja.upsvg.innerHTML = `
|
||||
${UP_ICON}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
function handleUploadSuccess(response, progressItem) {
|
||||
updateButtonVisibility(true);
|
||||
progressItem.innerHTML = `
|
||||
|
||||
<div class="ja-media-item">
|
||||
<div class="ja-item-thumbnail relative">
|
||||
<img src="${response.data.url}" alt="${response.data.name}" class="ja-thumbnail-image">
|
||||
<div class="ja-item-overlay">
|
||||
<button class="ja-copy-button" data-url="${response.data.url}"> ${COPY_ICON} </button>
|
||||
<button class="ja-preview-button" data-url="${response.data.url}"> ${PREVIEW_ICON} </button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ja-item-info">
|
||||
<div class="ja-item-details">
|
||||
<span class="ja-item-name">${response.data.name}</span>
|
||||
<span class="ja-upload-success">已完成</span>
|
||||
</div>
|
||||
<div class="ja-item-details">
|
||||
<span class="ja-item-size">${formatFileSize(response.data.size || 0)}</span>
|
||||
<span class="ja-item-dimensions">${response.data.width}*${response.data.height}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 确保响应中包含正确的URL
|
||||
if (!response.data || !response.data.url) {
|
||||
console.error('上传成功但缺少URL:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建新图片对象
|
||||
const newItem = {
|
||||
id: response.data.id || Date.now(),
|
||||
name: response.data.name,
|
||||
url: response.data.url,
|
||||
thumbnail: response.data.thumbnail || response.data.url,
|
||||
size: response.data.size || 0,
|
||||
dimensions: response.data.dimensions || '未知'
|
||||
};
|
||||
|
||||
// 将新图片添加到列表顶部
|
||||
state.mediaItems.unshift(newItem);
|
||||
state.totalItems = state.mediaItems.length;
|
||||
|
||||
// 如果当前是图库标签且在第一页,立即更新UI
|
||||
if (state.activeTab === 'gallery' && state.currentPage === 1) {
|
||||
renderMediaGrid();
|
||||
} else if (state.currentPage > 1) {
|
||||
// 如果不在第一页,更新总页数
|
||||
state.totalPages = Math.ceil(state.totalItems / 20);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传失败
|
||||
function handleUploadError(file, message, progressItem) {
|
||||
progressItem.className = 'ja-upload-item ja-upload-error';
|
||||
progressItem.innerHTML = `
|
||||
<div class="ja-upload-header">
|
||||
<span class="ja-upload-filename"></span>
|
||||
<span class="ja-upload-failure"></span>
|
||||
</div>
|
||||
<div class="ja-upload-error-message">${message}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function previewFile(url) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'ja-image-preview-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="ja-modal-content">
|
||||
<img src="${url}" alt="预览图片" class="ja-modal-image">
|
||||
<button class="ja-close-button">${CLOSE_ICON}</button>
|
||||
</div>
|
||||
`;
|
||||
const modalContent = modal.querySelector('.ja-modal-content');
|
||||
const image = modal.querySelector('.ja-modal-image');
|
||||
const closeButton = modal.querySelector('.ja-close-button');
|
||||
|
||||
// 关闭模态窗口
|
||||
closeButton.addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
// 点击背景关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.remove();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// 填充到表单
|
||||
function urlToPage(text) {
|
||||
if (typeof window.SetImageUrl === 'function') {
|
||||
SetImageUrl(text);
|
||||
} else {
|
||||
navigator.clipboard.writeText(text);
|
||||
showToast('没有找到接收函数,URL已复制到剪贴板', 'success');
|
||||
}
|
||||
toggleModal(); // 关闭模态框
|
||||
}
|
||||
|
||||
// 显示提示消息
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `ja-toast ja-toast-${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => {
|
||||
toast.classList.add('ja-toast-visible');
|
||||
}, 10);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('ja-toast-visible');
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(toast);
|
||||
}, 2000);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
createStyles();
|
||||
createUI();
|
||||
setupEventListeners();
|
||||
setVh();
|
||||
updateButtonVisibility(false);
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
init();
|
||||
|
||||
function SetImageUrl(url) {
|
||||
if (SetImageUrl.inputElement) {
|
||||
SetImageUrl.inputElement.value = url;
|
||||
}
|
||||
if (SetImageUrl.previewElement) {
|
||||
SetImageUrl.previewElement.src = url;
|
||||
}
|
||||
}
|
||||
|
||||
function OpenGallery(inputId, previewId) {
|
||||
SetImageUrl.inputElement = document.getElementById(inputId);
|
||||
SetImageUrl.previewElement = previewId !== undefined
|
||||
? document.getElementById(previewId)
|
||||
: null;
|
||||
toggleModal();
|
||||
}
|
||||
window.SetImageUrl = SetImageUrl;
|
||||
window.OpenGallery = OpenGallery;
|
||||
window.ja = {
|
||||
statusText: document.getElementById('ja-upload-status'),
|
||||
upsvg: document.getElementById('ja-upload-svg')
|
||||
};
|
||||
|
||||
})();
|
||||
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 169 KiB |